@sonordev/site-kit 6.3.5 → 6.4.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 (267) hide show
  1. package/agent-manifest.json +8 -2
  2. package/dist/{AnalyticsProvider-2A75HPK6.mjs → AnalyticsProvider-4K5T7OQ7.mjs} +6 -6
  3. package/dist/{AnalyticsProvider-2A75HPK6.mjs.map → AnalyticsProvider-4K5T7OQ7.mjs.map} +1 -1
  4. package/dist/{AnalyticsProvider-2H3POGSJ.js → AnalyticsProvider-QXZN5O6D.js} +10 -10
  5. package/dist/{AnalyticsProvider-2H3POGSJ.js.map → AnalyticsProvider-QXZN5O6D.js.map} +1 -1
  6. package/dist/{ArticleViewTracker-75BLDTHR.mjs → ArticleViewTracker-BUG2TOHS.mjs} +5 -5
  7. package/dist/{ArticleViewTracker-75BLDTHR.mjs.map → ArticleViewTracker-BUG2TOHS.mjs.map} +1 -1
  8. package/dist/{ArticleViewTracker-R3LNZ4A4.js → ArticleViewTracker-FFMHR7AY.js} +6 -6
  9. package/dist/{ArticleViewTracker-R3LNZ4A4.js.map → ArticleViewTracker-FFMHR7AY.js.map} +1 -1
  10. package/dist/{ChatWidget-F6UWIR7I.mjs → ChatWidget-CRQPBDEF.mjs} +7 -7
  11. package/dist/{ChatWidget-F6UWIR7I.mjs.map → ChatWidget-CRQPBDEF.mjs.map} +1 -1
  12. package/dist/{ChatWidget-BLZSLYN2.js → ChatWidget-XFYLVXAX.js} +10 -10
  13. package/dist/{ChatWidget-BLZSLYN2.js.map → ChatWidget-XFYLVXAX.js.map} +1 -1
  14. package/dist/EngageWidget-S234YAT2.js +21 -0
  15. package/dist/{EngageWidget-YONW3W3Z.js.map → EngageWidget-S234YAT2.js.map} +1 -1
  16. package/dist/EngageWidget-SQU4T7CI.mjs +12 -0
  17. package/dist/{EngageWidget-4WB74GQR.mjs.map → EngageWidget-SQU4T7CI.mjs.map} +1 -1
  18. package/dist/{FileField-GXNFYVSY.mjs → FileField-HNEBEBLB.mjs} +5 -5
  19. package/dist/{FileField-GXNFYVSY.mjs.map → FileField-HNEBEBLB.mjs.map} +1 -1
  20. package/dist/{FileField-KYQVN4XB.js → FileField-XG4EYAZW.js} +6 -6
  21. package/dist/{FileField-KYQVN4XB.js.map → FileField-XG4EYAZW.js.map} +1 -1
  22. package/dist/{FormSpotlight-ZOSI7MXH.js → FormSpotlight-IODREOPN.js} +9 -9
  23. package/dist/{FormSpotlight-ZOSI7MXH.js.map → FormSpotlight-IODREOPN.js.map} +1 -1
  24. package/dist/{FormSpotlight-F4YZOWHA.mjs → FormSpotlight-VKD774PZ.mjs} +3 -3
  25. package/dist/{FormSpotlight-F4YZOWHA.mjs.map → FormSpotlight-VKD774PZ.mjs.map} +1 -1
  26. package/dist/{FormStage-DICKOW7R.js → FormStage-37KKZQH4.js} +9 -9
  27. package/dist/{FormStage-DICKOW7R.js.map → FormStage-37KKZQH4.js.map} +1 -1
  28. package/dist/{FormStage-TIQ43MF6.mjs → FormStage-PRZNRYNC.mjs} +3 -3
  29. package/dist/{FormStage-TIQ43MF6.mjs.map → FormStage-PRZNRYNC.mjs.map} +1 -1
  30. package/dist/ManagedForm-4PZOCKCV.mjs +16 -0
  31. package/dist/{ManagedForm-KKJOIEFW.mjs.map → ManagedForm-4PZOCKCV.mjs.map} +1 -1
  32. package/dist/ManagedForm-JCMEE626.js +25 -0
  33. package/dist/{ManagedForm-ZUPV4DPQ.js.map → ManagedForm-JCMEE626.js.map} +1 -1
  34. package/dist/{ManagedNewsletterForm-NFV2JGNA.js → ManagedNewsletterForm-CTC33B2U.js} +7 -7
  35. package/dist/{ManagedNewsletterForm-NFV2JGNA.js.map → ManagedNewsletterForm-CTC33B2U.js.map} +1 -1
  36. package/dist/{ManagedNewsletterForm-JOTIWXLQ.mjs → ManagedNewsletterForm-KMZG24OE.mjs} +6 -6
  37. package/dist/{ManagedNewsletterForm-JOTIWXLQ.mjs.map → ManagedNewsletterForm-KMZG24OE.mjs.map} +1 -1
  38. package/dist/{SignalCore-DLRQTFKK.js → SignalCore-YHT7WPXV.js} +11 -11
  39. package/dist/{SignalCore-DLRQTFKK.js.map → SignalCore-YHT7WPXV.js.map} +1 -1
  40. package/dist/{SignalCore-MUN44S6K.mjs → SignalCore-YIPKKIUG.mjs} +5 -5
  41. package/dist/{SignalCore-MUN44S6K.mjs.map → SignalCore-YIPKKIUG.mjs.map} +1 -1
  42. package/dist/SitemapSync-GN4OXIM2.js +18 -0
  43. package/dist/{SitemapSync-5TKLHXFL.js.map → SitemapSync-GN4OXIM2.js.map} +1 -1
  44. package/dist/SitemapSync-NG763CGN.mjs +9 -0
  45. package/dist/{SitemapSync-BIWCTMRJ.mjs.map → SitemapSync-NG763CGN.mjs.map} +1 -1
  46. package/dist/analytics/AnalyticsProvider.d.ts +0 -6
  47. package/dist/analytics/AnalyticsProvider.d.ts.map +1 -1
  48. package/dist/analytics/WebVitals.d.ts +4 -1
  49. package/dist/analytics/WebVitals.d.ts.map +1 -1
  50. package/dist/analytics/index.js +9 -9
  51. package/dist/analytics/index.mjs +4 -4
  52. package/dist/analytics/measurement-context.d.ts +13 -0
  53. package/dist/analytics/measurement-context.d.ts.map +1 -0
  54. package/dist/analytics/page-observation.d.ts +39 -0
  55. package/dist/analytics/page-observation.d.ts.map +1 -0
  56. package/dist/articles/AuthorPage.d.ts +1 -1
  57. package/dist/articles/AuthorPage.d.ts.map +1 -1
  58. package/dist/articles/author-schema.d.ts +59 -13
  59. package/dist/articles/author-schema.d.ts.map +1 -1
  60. package/dist/articles/index.js +24 -23
  61. package/dist/articles/index.js.map +1 -1
  62. package/dist/articles/index.mjs +12 -11
  63. package/dist/articles/index.mjs.map +1 -1
  64. package/dist/articles/routes.d.ts +9 -0
  65. package/dist/articles/routes.d.ts.map +1 -1
  66. package/dist/articles/server-core.d.ts +5 -2
  67. package/dist/articles/server-core.d.ts.map +1 -1
  68. package/dist/articles/server-ui.js +45 -45
  69. package/dist/articles/server-ui.mjs +8 -8
  70. package/dist/articles/server.d.ts +5 -2
  71. package/dist/articles/server.d.ts.map +1 -1
  72. package/dist/articles/server.js +72 -56
  73. package/dist/articles/server.mjs +3 -3
  74. package/dist/articles/types.d.ts +17 -0
  75. package/dist/articles/types.d.ts.map +1 -1
  76. package/dist/{chunk-H6F7T5Z6.mjs → chunk-24LFAQEG.mjs} +5 -5
  77. package/dist/{chunk-H6F7T5Z6.mjs.map → chunk-24LFAQEG.mjs.map} +1 -1
  78. package/dist/chunk-3AOKYAZ2.js +135 -0
  79. package/dist/chunk-3AOKYAZ2.js.map +1 -0
  80. package/dist/{chunk-QII2TNKS.mjs → chunk-4C3INRHE.mjs} +72 -15
  81. package/dist/chunk-4C3INRHE.mjs.map +1 -0
  82. package/dist/{chunk-ZR43XFUM.js → chunk-55J2KE7D.js} +3 -3
  83. package/dist/chunk-55J2KE7D.js.map +1 -0
  84. package/dist/{chunk-SIQX3GL3.mjs → chunk-5BARLEAI.mjs} +3 -3
  85. package/dist/{chunk-SIQX3GL3.mjs.map → chunk-5BARLEAI.mjs.map} +1 -1
  86. package/dist/{chunk-HTCMZAXW.mjs → chunk-5T3J24K4.mjs} +4 -4
  87. package/dist/{chunk-HTCMZAXW.mjs.map → chunk-5T3J24K4.mjs.map} +1 -1
  88. package/dist/{chunk-RWERWLRM.js → chunk-5W4S7XQD.js} +3 -3
  89. package/dist/{chunk-RWERWLRM.js.map → chunk-5W4S7XQD.js.map} +1 -1
  90. package/dist/{chunk-6DCAMW62.mjs → chunk-6C47333H.mjs} +3 -3
  91. package/dist/chunk-6C47333H.mjs.map +1 -0
  92. package/dist/{chunk-2OA274JZ.mjs → chunk-6QPKC6XP.mjs} +3 -3
  93. package/dist/{chunk-2OA274JZ.mjs.map → chunk-6QPKC6XP.mjs.map} +1 -1
  94. package/dist/{chunk-R7XCEUEI.mjs → chunk-BI7DH42Z.mjs} +4 -4
  95. package/dist/{chunk-R7XCEUEI.mjs.map → chunk-BI7DH42Z.mjs.map} +1 -1
  96. package/dist/{chunk-ID4RGBQT.js → chunk-C7J4XLBN.js} +15 -15
  97. package/dist/{chunk-ID4RGBQT.js.map → chunk-C7J4XLBN.js.map} +1 -1
  98. package/dist/{chunk-LWCAZDY2.js → chunk-CDRQYJUT.js} +4 -4
  99. package/dist/{chunk-LWCAZDY2.js.map → chunk-CDRQYJUT.js.map} +1 -1
  100. package/dist/chunk-CIB3RHNZ.js +8 -0
  101. package/dist/{chunk-LSB3CWSB.js.map → chunk-CIB3RHNZ.js.map} +1 -1
  102. package/dist/{chunk-NCLF7RUL.js → chunk-CUZ3I6CO.js} +5 -5
  103. package/dist/{chunk-NCLF7RUL.js.map → chunk-CUZ3I6CO.js.map} +1 -1
  104. package/dist/{chunk-UEYZXWIP.js → chunk-EWDWLY74.js} +11 -11
  105. package/dist/{chunk-UEYZXWIP.js.map → chunk-EWDWLY74.js.map} +1 -1
  106. package/dist/{chunk-4EMMMKDR.js → chunk-F2GECCK7.js} +12 -12
  107. package/dist/{chunk-4EMMMKDR.js.map → chunk-F2GECCK7.js.map} +1 -1
  108. package/dist/chunk-GFSQMK35.mjs +127 -0
  109. package/dist/chunk-GFSQMK35.mjs.map +1 -0
  110. package/dist/{chunk-XP63NBUV.js → chunk-GGGX54ZE.js} +14 -14
  111. package/dist/{chunk-XP63NBUV.js.map → chunk-GGGX54ZE.js.map} +1 -1
  112. package/dist/{chunk-ZPITTAT5.mjs → chunk-GSU6VTCZ.mjs} +3 -3
  113. package/dist/{chunk-ZPITTAT5.mjs.map → chunk-GSU6VTCZ.mjs.map} +1 -1
  114. package/dist/{chunk-XQPNCDGF.js → chunk-GXSSFQ5D.js} +15 -15
  115. package/dist/{chunk-XQPNCDGF.js.map → chunk-GXSSFQ5D.js.map} +1 -1
  116. package/dist/{chunk-BJ4T65OK.mjs → chunk-H4TSADY3.mjs} +3 -3
  117. package/dist/{chunk-BJ4T65OK.mjs.map → chunk-H4TSADY3.mjs.map} +1 -1
  118. package/dist/{chunk-3UT3XR6M.js → chunk-HSLPN3QK.js} +7 -7
  119. package/dist/{chunk-3UT3XR6M.js.map → chunk-HSLPN3QK.js.map} +1 -1
  120. package/dist/{chunk-QAYB7NCH.js → chunk-HTCNRTN6.js} +129 -69
  121. package/dist/chunk-HTCNRTN6.js.map +1 -0
  122. package/dist/{chunk-Y3SYVAP6.mjs → chunk-JXRGIE3V.mjs} +4 -4
  123. package/dist/{chunk-Y3SYVAP6.mjs.map → chunk-JXRGIE3V.mjs.map} +1 -1
  124. package/dist/chunk-K3PVUXKB.mjs +6 -0
  125. package/dist/{chunk-7JGD5TUI.mjs.map → chunk-K3PVUXKB.mjs.map} +1 -1
  126. package/dist/{chunk-L2KN3GYW.mjs → chunk-KOPFFKG2.mjs} +3 -3
  127. package/dist/{chunk-L2KN3GYW.mjs.map → chunk-KOPFFKG2.mjs.map} +1 -1
  128. package/dist/{chunk-LP6KJPAL.js → chunk-KZBDH5A3.js} +4 -4
  129. package/dist/{chunk-LP6KJPAL.js.map → chunk-KZBDH5A3.js.map} +1 -1
  130. package/dist/{chunk-DJAJB6AZ.mjs → chunk-LPDXS2B2.mjs} +3 -3
  131. package/dist/{chunk-DJAJB6AZ.mjs.map → chunk-LPDXS2B2.mjs.map} +1 -1
  132. package/dist/{chunk-I5X7BX6I.mjs → chunk-M3JCHPK3.mjs} +127 -67
  133. package/dist/chunk-M3JCHPK3.mjs.map +1 -0
  134. package/dist/{chunk-RSAQ7WGF.mjs → chunk-M62L2N5A.mjs} +3 -3
  135. package/dist/{chunk-RSAQ7WGF.mjs.map → chunk-M62L2N5A.mjs.map} +1 -1
  136. package/dist/{chunk-G3L26XQ7.js → chunk-OIPG3EGM.js} +5 -5
  137. package/dist/{chunk-G3L26XQ7.js.map → chunk-OIPG3EGM.js.map} +1 -1
  138. package/dist/{chunk-X3MY4PL7.mjs → chunk-QAQTDHHT.mjs} +10 -14
  139. package/dist/chunk-QAQTDHHT.mjs.map +1 -0
  140. package/dist/{chunk-VFXLB5JU.mjs → chunk-QOKMH2TD.mjs} +3 -3
  141. package/dist/{chunk-VFXLB5JU.mjs.map → chunk-QOKMH2TD.mjs.map} +1 -1
  142. package/dist/{chunk-HXHRS3PX.mjs → chunk-RCB6HEHM.mjs} +3 -3
  143. package/dist/{chunk-HXHRS3PX.mjs.map → chunk-RCB6HEHM.mjs.map} +1 -1
  144. package/dist/{chunk-NAA5J2JM.js → chunk-RQROZLH3.js} +47 -51
  145. package/dist/chunk-RQROZLH3.js.map +1 -0
  146. package/dist/{chunk-PHE5MIW4.js → chunk-S5QKNXLQ.js} +10 -10
  147. package/dist/{chunk-PHE5MIW4.js.map → chunk-S5QKNXLQ.js.map} +1 -1
  148. package/dist/{chunk-MERNU3MY.mjs → chunk-SBTA6B6Y.mjs} +4 -4
  149. package/dist/{chunk-MERNU3MY.mjs.map → chunk-SBTA6B6Y.mjs.map} +1 -1
  150. package/dist/{chunk-CPRCBO6Y.mjs → chunk-TPDDSEL3.mjs} +3 -3
  151. package/dist/{chunk-CPRCBO6Y.mjs.map → chunk-TPDDSEL3.mjs.map} +1 -1
  152. package/dist/{chunk-C4JCJ66V.js → chunk-U4CYO4NG.js} +5 -5
  153. package/dist/{chunk-C4JCJ66V.js.map → chunk-U4CYO4NG.js.map} +1 -1
  154. package/dist/{chunk-BVSTYFFI.js → chunk-U5NFIHLY.js} +4 -4
  155. package/dist/{chunk-BVSTYFFI.js.map → chunk-U5NFIHLY.js.map} +1 -1
  156. package/dist/{chunk-4KTPBCB3.mjs → chunk-USQPRLSG.mjs} +8 -8
  157. package/dist/{chunk-4KTPBCB3.mjs.map → chunk-USQPRLSG.mjs.map} +1 -1
  158. package/dist/{chunk-RR6U3Z76.js → chunk-VDEQVXNV.js} +3 -3
  159. package/dist/{chunk-RR6U3Z76.js.map → chunk-VDEQVXNV.js.map} +1 -1
  160. package/dist/{chunk-WYUYAMFL.js → chunk-VMNPLCZB.js} +14 -14
  161. package/dist/{chunk-WYUYAMFL.js.map → chunk-VMNPLCZB.js.map} +1 -1
  162. package/dist/{chunk-5EVGWCUB.js → chunk-W7IAG5YD.js} +3 -3
  163. package/dist/{chunk-5EVGWCUB.js.map → chunk-W7IAG5YD.js.map} +1 -1
  164. package/dist/{chunk-K5Q7UYZM.mjs → chunk-WIWHM47B.mjs} +3 -3
  165. package/dist/{chunk-K5Q7UYZM.mjs.map → chunk-WIWHM47B.mjs.map} +1 -1
  166. package/dist/{chunk-KRU755E3.js → chunk-XZHR4E2D.js} +8 -8
  167. package/dist/{chunk-KRU755E3.js.map → chunk-XZHR4E2D.js.map} +1 -1
  168. package/dist/{chunk-EW2GKDDC.js → chunk-YACU35MY.js} +9 -9
  169. package/dist/{chunk-EW2GKDDC.js.map → chunk-YACU35MY.js.map} +1 -1
  170. package/dist/chunk-YMZNEDFP.js +39 -0
  171. package/dist/chunk-YMZNEDFP.js.map +1 -0
  172. package/dist/{chunk-QMVBKSTZ.mjs → chunk-YRKWJCML.mjs} +3 -3
  173. package/dist/{chunk-QMVBKSTZ.mjs.map → chunk-YRKWJCML.mjs.map} +1 -1
  174. package/dist/chunk-Z2URIOKG.mjs +36 -0
  175. package/dist/chunk-Z2URIOKG.mjs.map +1 -0
  176. package/dist/{chunk-S2LJ26WQ.mjs → chunk-ZEBOOUBP.mjs} +3 -3
  177. package/dist/{chunk-S2LJ26WQ.mjs.map → chunk-ZEBOOUBP.mjs.map} +1 -1
  178. package/dist/{chunk-QXD7AWGV.js → chunk-ZOBUA65N.js} +74 -17
  179. package/dist/chunk-ZOBUA65N.js.map +1 -0
  180. package/dist/cli/index.js +3 -3
  181. package/dist/cli/index.mjs +1 -1
  182. package/dist/client/index.js +4 -4
  183. package/dist/client/index.mjs +3 -3
  184. package/dist/commerce/index.js +57 -57
  185. package/dist/commerce/index.mjs +5 -5
  186. package/dist/engage/DesignRenderer.d.ts.map +1 -1
  187. package/dist/engage/EngageWidget.d.ts.map +1 -1
  188. package/dist/engage/element-rules.d.ts +39 -0
  189. package/dist/engage/element-rules.d.ts.map +1 -0
  190. package/dist/engage/index.js +9 -9
  191. package/dist/engage/index.mjs +6 -6
  192. package/dist/fleet/index.js +8 -8
  193. package/dist/fleet/index.mjs +4 -4
  194. package/dist/forms/index.js +13 -13
  195. package/dist/forms/index.mjs +7 -7
  196. package/dist/forms/server.js +2 -2
  197. package/dist/forms/server.mjs +1 -1
  198. package/dist/images/index.js +11 -11
  199. package/dist/images/index.mjs +4 -4
  200. package/dist/index.js +63 -63
  201. package/dist/index.mjs +9 -9
  202. package/dist/layout/client.js +8 -8
  203. package/dist/layout/client.mjs +7 -7
  204. package/dist/layout/index.js +10 -10
  205. package/dist/layout/index.mjs +8 -8
  206. package/dist/llms/index.d.ts +2 -0
  207. package/dist/llms/index.d.ts.map +1 -1
  208. package/dist/llms/index.js +129 -14
  209. package/dist/llms/index.js.map +1 -1
  210. package/dist/llms/index.mjs +115 -5
  211. package/dist/llms/index.mjs.map +1 -1
  212. package/dist/llms/revalidate.d.ts +3 -0
  213. package/dist/llms/revalidate.d.ts.map +1 -1
  214. package/dist/llms/seo-revalidate.d.ts +105 -0
  215. package/dist/llms/seo-revalidate.d.ts.map +1 -0
  216. package/dist/maps/index.js +5 -5
  217. package/dist/maps/index.mjs +3 -3
  218. package/dist/mcp/transport.d.ts +166 -0
  219. package/dist/mcp/transport.d.ts.map +1 -0
  220. package/dist/mcp/transport.js +137 -0
  221. package/dist/mcp/transport.js.map +1 -0
  222. package/dist/mcp/transport.mjs +123 -0
  223. package/dist/mcp/transport.mjs.map +1 -0
  224. package/dist/search/index.js +4 -4
  225. package/dist/search/index.mjs +3 -3
  226. package/dist/seo/client.js +5 -5
  227. package/dist/seo/client.mjs +4 -4
  228. package/dist/seo/index.js +5 -5
  229. package/dist/seo/index.mjs +4 -4
  230. package/dist/seo/register-sitemap-cli.js +1 -1
  231. package/dist/seo/register-sitemap-cli.mjs +1 -1
  232. package/dist/server/index.js +3 -3
  233. package/dist/server/index.mjs +2 -2
  234. package/dist/shared/build-entries.d.ts +7 -0
  235. package/dist/shared/build-entries.d.ts.map +1 -1
  236. package/dist/shared/request-guards.d.ts +27 -0
  237. package/dist/shared/request-guards.d.ts.map +1 -0
  238. package/dist/shared/version.d.ts +1 -1
  239. package/dist/signal/index.js +11 -11
  240. package/dist/signal/index.mjs +2 -2
  241. package/dist/sitemap/index.js +3 -3
  242. package/dist/sitemap/index.mjs +2 -2
  243. package/dist/sync/index.js +17 -17
  244. package/dist/sync/index.mjs +4 -4
  245. package/dist/{writeLLMsTxt-LYYC3ZSN.mjs → writeLLMsTxt-IYJILIU3.mjs} +3 -3
  246. package/dist/{writeLLMsTxt-XHLHJ5XY.js → writeLLMsTxt-SQG4B63X.js} +4 -4
  247. package/package.json +6 -1
  248. package/dist/EngageWidget-4WB74GQR.mjs +0 -12
  249. package/dist/EngageWidget-YONW3W3Z.js +0 -21
  250. package/dist/ManagedForm-KKJOIEFW.mjs +0 -16
  251. package/dist/ManagedForm-ZUPV4DPQ.js +0 -25
  252. package/dist/SitemapSync-5TKLHXFL.js +0 -18
  253. package/dist/SitemapSync-BIWCTMRJ.mjs +0 -9
  254. package/dist/chunk-6DCAMW62.mjs.map +0 -1
  255. package/dist/chunk-7JGD5TUI.mjs +0 -6
  256. package/dist/chunk-I5X7BX6I.mjs.map +0 -1
  257. package/dist/chunk-LSB3CWSB.js +0 -8
  258. package/dist/chunk-NAA5J2JM.js.map +0 -1
  259. package/dist/chunk-QAYB7NCH.js.map +0 -1
  260. package/dist/chunk-QII2TNKS.mjs.map +0 -1
  261. package/dist/chunk-QXD7AWGV.js.map +0 -1
  262. package/dist/chunk-X3MY4PL7.mjs.map +0 -1
  263. package/dist/chunk-YRCR7WY3.js +0 -65
  264. package/dist/chunk-YRCR7WY3.js.map +0 -1
  265. package/dist/chunk-ZL35JCDA.mjs +0 -61
  266. package/dist/chunk-ZL35JCDA.mjs.map +0 -1
  267. package/dist/chunk-ZR43XFUM.js.map +0 -1
@@ -1 +1 @@
1
- {"version":3,"file":"revalidate.d.ts","sourceRoot":"","sources":["../../src/llms/revalidate.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;GAaG;AAEH,KAAK,gBAAgB,GAAG,CAAC,IAAI,EAAE,MAAM,EAAE,IAAI,CAAC,EAAE,MAAM,GAAG,QAAQ,KAAK,IAAI,CAAA;AAExE;;;GAGG;AACH,wBAAgB,2BAA2B,CACzC,MAAM,EAAE,MAAM,EACd,OAAO,CAAC,EAAE;IACR,0DAA0D;IAC1D,UAAU,CAAC,EAAE,MAAM,EAAE,CAAA;IACrB,2EAA2E;IAC3E,cAAc,CAAC,EAAE,gBAAgB,CAAA;CAClC,GACA,CAAC,OAAO,EAAE,OAAO,KAAK,OAAO,CAAC,QAAQ,CAAC,CA6CzC"}
1
+ {"version":3,"file":"revalidate.d.ts","sourceRoot":"","sources":["../../src/llms/revalidate.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;GAgBG;AAIH,KAAK,gBAAgB,GAAG,CAAC,IAAI,EAAE,MAAM,EAAE,IAAI,CAAC,EAAE,MAAM,GAAG,QAAQ,KAAK,IAAI,CAAA;AAExE;;;GAGG;AACH,wBAAgB,2BAA2B,CACzC,MAAM,EAAE,MAAM,EACd,OAAO,CAAC,EAAE;IACR,0DAA0D;IAC1D,UAAU,CAAC,EAAE,MAAM,EAAE,CAAA;IACrB,2EAA2E;IAC3E,cAAc,CAAC,EAAE,gBAAgB,CAAA;CAClC,GACA,CAAC,OAAO,EAAE,OAAO,KAAK,OAAO,CAAC,QAAQ,CAAC,CA6CzC"}
@@ -0,0 +1,105 @@
1
+ /**
2
+ * @sonordev/site-kit/llms - Sonor's SEO revalidation webhook
3
+ *
4
+ * When a title, description or schema changes in Sonor (a dashboard edit or an
5
+ * approved SEO ledger action), Sonor POSTs the affected paths and cache tags to
6
+ * the site, authenticated with the project key. This handler regenerates those
7
+ * pages, the sitemap and both llms files without a rebuild.
8
+ *
9
+ * Single source of truth for the fleet's `/api/seo-revalidate` handler. It
10
+ * replaced per-site copies in heinrich-law-nextjs and wirsch-law-nextjs
11
+ * (`lib/seo-revalidation.ts`), and agency-site-kit's portfolio
12
+ * `createRevalidateHandler` is built on it (`extraPaths` + `extendPayload`);
13
+ * don't fork it back into a site or package.
14
+ *
15
+ * @example
16
+ * ```ts
17
+ * // app/api/seo-revalidate/route.ts
18
+ * import { revalidatePath, revalidateTag } from 'next/cache'
19
+ * import { createSeoRevalidationHandler } from '@sonordev/site-kit/llms'
20
+ *
21
+ * export const runtime = 'nodejs'
22
+ *
23
+ * export async function POST(request: Request) {
24
+ * return createSeoRevalidationHandler({
25
+ * secret: process.env.SONOR_API_KEY || '',
26
+ * revalidatePath,
27
+ * revalidateTag,
28
+ * publicationBasePath: '/insights', // only for sites with a publication
29
+ * })(request)
30
+ * }
31
+ * ```
32
+ *
33
+ * Contract (POST, `Authorization: Bearer <SONOR_API_KEY>`, JSON):
34
+ * `{ paths?: string[], path?: string, tags?: string[], tag?: string, revalidateAll?: boolean }`
35
+ */
36
+ /** Largest webhook body accepted, in bytes. Sonor's payloads are a few hundred. */
37
+ export declare const SEO_REVALIDATION_MAX_BODY_BYTES: number;
38
+ /** Most paths or tags one webhook call may name. */
39
+ export declare const SEO_REVALIDATION_MAX_ITEMS = 100;
40
+ /**
41
+ * The prefix Sonor projects configured before publication routing still send
42
+ * article URLs under. See `publicationBasePath`.
43
+ */
44
+ export declare const LEGACY_PUBLICATION_PATH = "/blog";
45
+ export interface SeoRevalidationHandlerOptions {
46
+ /**
47
+ * The project key Sonor signs the webhook with (`SONOR_API_KEY`), or a
48
+ * getter read on every call. Empty or missing refuses every call.
49
+ */
50
+ secret: string | (() => string | undefined);
51
+ /** Next's `revalidatePath` from `next/cache`. */
52
+ revalidatePath: (path: string, type?: 'page' | 'layout') => void;
53
+ /** Next's `revalidateTag` from `next/cache`. Called with `{ expire: 0 }` so tagged fetches expire now. */
54
+ revalidateTag: (tag: string, profile: {
55
+ expire: number;
56
+ }) => void;
57
+ /**
58
+ * Where the site's publication (articles) lives, e.g. `/insights`. Set it
59
+ * only when the site has one. The publication index and its RSS/Atom feeds
60
+ * are then refreshed on every call, and legacy `/blog/...` paths Sonor sends
61
+ * are refreshed at both the alias and the matching URL under this root.
62
+ */
63
+ publicationBasePath?: string;
64
+ /**
65
+ * Local paths regenerated on every call, next to `/sitemap.xml` (e.g. a
66
+ * portfolio hub like `/work`). Validated like payload paths.
67
+ */
68
+ extraPaths?: string[];
69
+ /**
70
+ * Extend the validated payload from fields of the raw body this handler
71
+ * doesn't read. agency-site-kit maps portfolio `slug`/`slugs` to detail
72
+ * paths and names a default tag this way. The result is validated again,
73
+ * so an extension can't widen what a caller may revalidate; a throw is a 400.
74
+ */
75
+ extendPayload?: (payload: SeoRevalidationPayload, body: Record<string, unknown>) => SeoRevalidationPayload;
76
+ }
77
+ export interface SeoRevalidationPayload {
78
+ /** Local paths to regenerate, deduped. */
79
+ paths: string[];
80
+ /** Cache tags to expire, deduped. */
81
+ tags: string[];
82
+ /** Regenerate everything (the root layout). */
83
+ revalidateAll: boolean;
84
+ }
85
+ /**
86
+ * Validate a webhook body. Throws on anything malformed, so a bad payload is
87
+ * refused whole rather than half-applied.
88
+ */
89
+ export declare function parseSeoRevalidationPayload(value: unknown): SeoRevalidationPayload;
90
+ /**
91
+ * Create the POST handler for Sonor's SEO revalidation webhook.
92
+ *
93
+ * - 401 unless `Authorization: Bearer <secret>` matches (constant time). The
94
+ * `?secret=` query form `createLlmsRevalidateHandler` accepts is not
95
+ * honoured here: keys don't belong in URLs.
96
+ * - 413 past 16 KB, 400 on any path or tag that fails validation. Nothing is
97
+ * revalidated in either case.
98
+ * - Otherwise regenerates the named pages (or the root layout for a full or
99
+ * tag-only `seo` refresh), `/sitemap.xml`, `/llms.txt`, `/llms-full.txt`,
100
+ * `extraPaths` and, with `publicationBasePath`, the publication index and
101
+ * feeds; then expires the named cache tags. `extendPayload` runs before any
102
+ * of that, and its result is validated like the body.
103
+ */
104
+ export declare function createSeoRevalidationHandler({ secret: secretOption, revalidatePath, revalidateTag, publicationBasePath, extraPaths: extraPathsOption, extendPayload, }: SeoRevalidationHandlerOptions): (request: Request) => Promise<Response>;
105
+ //# sourceMappingURL=seo-revalidate.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"seo-revalidate.d.ts","sourceRoot":"","sources":["../../src/llms/seo-revalidate.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAkCG;AAKH,mFAAmF;AACnF,eAAO,MAAM,+BAA+B,QAAY,CAAA;AAExD,oDAAoD;AACpD,eAAO,MAAM,0BAA0B,MAAM,CAAA;AAE7C;;;GAGG;AACH,eAAO,MAAM,uBAAuB,UAAU,CAAA;AAE9C,MAAM,WAAW,6BAA6B;IAC5C;;;OAGG;IACH,MAAM,EAAE,MAAM,GAAG,CAAC,MAAM,MAAM,GAAG,SAAS,CAAC,CAAA;IAC3C,iDAAiD;IACjD,cAAc,EAAE,CAAC,IAAI,EAAE,MAAM,EAAE,IAAI,CAAC,EAAE,MAAM,GAAG,QAAQ,KAAK,IAAI,CAAA;IAChE,0GAA0G;IAC1G,aAAa,EAAE,CAAC,GAAG,EAAE,MAAM,EAAE,OAAO,EAAE;QAAE,MAAM,EAAE,MAAM,CAAA;KAAE,KAAK,IAAI,CAAA;IACjE;;;;;OAKG;IACH,mBAAmB,CAAC,EAAE,MAAM,CAAA;IAC5B;;;OAGG;IACH,UAAU,CAAC,EAAE,MAAM,EAAE,CAAA;IACrB;;;;;OAKG;IACH,aAAa,CAAC,EAAE,CAAC,OAAO,EAAE,sBAAsB,EAAE,IAAI,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,KAAK,sBAAsB,CAAA;CAC3G;AAED,MAAM,WAAW,sBAAsB;IACrC,0CAA0C;IAC1C,KAAK,EAAE,MAAM,EAAE,CAAA;IACf,qCAAqC;IACrC,IAAI,EAAE,MAAM,EAAE,CAAA;IACd,+CAA+C;IAC/C,aAAa,EAAE,OAAO,CAAA;CACvB;AA+BD;;;GAGG;AACH,wBAAgB,2BAA2B,CAAC,KAAK,EAAE,OAAO,GAAG,sBAAsB,CAmBlF;AAoBD;;;;;;;;;;;;;GAaG;AACH,wBAAgB,4BAA4B,CAAC,EAC3C,MAAM,EAAE,YAAY,EACpB,cAAc,EACd,aAAa,EACb,mBAAmB,EACnB,UAAU,EAAE,gBAAgB,EAC5B,aAAa,GACd,EAAE,6BAA6B,GAAG,CAAC,OAAO,EAAE,OAAO,KAAK,OAAO,CAAC,QAAQ,CAAC,CA6DzE"}
@@ -1,9 +1,9 @@
1
1
  'use client';
2
2
  'use strict';
3
3
 
4
- var chunkID4RGBQT_js = require('../chunk-ID4RGBQT.js');
5
- require('../chunk-BVSTYFFI.js');
6
- require('../chunk-LSB3CWSB.js');
4
+ var chunkC7J4XLBN_js = require('../chunk-C7J4XLBN.js');
5
+ require('../chunk-U5NFIHLY.js');
6
+ require('../chunk-CIB3RHNZ.js');
7
7
  require('../chunk-SNAQBZPT.js');
8
8
  var react = require('react');
9
9
  var reactGoogleMaps = require('@vis.gl/react-google-maps');
@@ -141,7 +141,7 @@ async function fetchMapsConfig() {
141
141
  const { apiUrl, apiKey } = getApiConfig();
142
142
  if (apiKey) {
143
143
  try {
144
- const res = await chunkID4RGBQT_js.sonorFetch(
144
+ const res = await chunkC7J4XLBN_js.sonorFetch(
145
145
  `${apiUrl}/api/public/maps/config`,
146
146
  { headers: { "Content-Type": "application/json" } },
147
147
  { apiKey }
@@ -181,7 +181,7 @@ async function fetchNearbyPlaces(params) {
181
181
  });
182
182
  if (params.radius) searchParams.set("radius", String(params.radius));
183
183
  if (params.limit) searchParams.set("limit", String(params.limit));
184
- const res = await chunkID4RGBQT_js.sonorFetch(
184
+ const res = await chunkC7J4XLBN_js.sonorFetch(
185
185
  `${apiUrl}/api/public/maps/nearby-places?${searchParams}`,
186
186
  { headers: { "Content-Type": "application/json" } },
187
187
  { apiKey }
@@ -1,7 +1,7 @@
1
1
  'use client';
2
- import { sonorFetch } from '../chunk-HTCMZAXW.mjs';
3
- import '../chunk-QMVBKSTZ.mjs';
4
- import '../chunk-7JGD5TUI.mjs';
2
+ import { sonorFetch } from '../chunk-5T3J24K4.mjs';
3
+ import '../chunk-YRKWJCML.mjs';
4
+ import '../chunk-K3PVUXKB.mjs';
5
5
  import '../chunk-BEXTDHO4.mjs';
6
6
  import { useState, useCallback, useEffect } from 'react';
7
7
  import { APIProvider, Map as Map$1, AdvancedMarker, Pin, InfoWindow } from '@vis.gl/react-google-maps';
@@ -0,0 +1,166 @@
1
+ /**
2
+ * @sonordev/site-kit/mcp/transport — the rate-limited public MCP relay
3
+ *
4
+ * Netlify rate-limits a native function (declarative `config.rateLimit`), never
5
+ * a Next.js route handler. So a public MCP endpoint on Netlify takes this path:
6
+ *
7
+ * agent ─► /api/mcp netlify/functions/mcp.mjs, rate-limited
8
+ * │ createNetlifyMcpRelay: signs the request
9
+ * │ (HMAC-SHA256 of MCP_TRANSPORT_SECRET over a label)
10
+ * ▼
11
+ * https://<deploy-id>--<site>.netlify.app/api/mcp-internal
12
+ * │ createMcpInternalRoute: 403 unless signed,
13
+ * ▼ then dispatches to the /api/mcp handlers
14
+ * app/api/mcp/route.ts protectMcpHandlers(createMcpHandler(...))
15
+ *
16
+ * In production the function owns /api/mcp, and the Next route behind it
17
+ * refuses unsigned calls too, so no request reaches a tool without passing
18
+ * the rate limit. The relay targets the deploy permalink, so a request is
19
+ * answered by the same immutable deploy that received it, and the base URL
20
+ * comes from the platform (`context`), never from a client header.
21
+ *
22
+ * Single source of truth for the relay. It replaced per-site copies in
23
+ * upforge-nextjs and wirsch-law-nextjs (`lib/mcp/transport-auth.ts`, the
24
+ * `protect()` wrapper, the `/api/mcp-internal` relay and the function body);
25
+ * don't fork it back into a site.
26
+ *
27
+ * Node only (`crypto`), and deliberately not re-exported from the
28
+ * `@sonordev/site-kit/mcp` barrel so that barrel stays runtime-neutral.
29
+ * Nothing here imports `server-only`: the Netlify function runs in plain Node,
30
+ * where `server-only` throws on import.
31
+ *
32
+ * Setup: `MCP_TRANSPORT_SECRET` (`openssl rand -hex 32`) in every deploy
33
+ * context, Functions scope. See the module README for the three files.
34
+ */
35
+ /** Env var holding the relay's HMAC key. */
36
+ export declare const MCP_TRANSPORT_SECRET_ENV = "MCP_TRANSPORT_SECRET";
37
+ /** Default request header carrying the signature (and the response marker). */
38
+ export declare const DEFAULT_MCP_TRANSPORT_HEADER = "x-site-mcp-transport";
39
+ /** Default HMAC message. Changing it invalidates every signature at once. */
40
+ export declare const DEFAULT_MCP_TRANSPORT_LABEL = "site-mcp-transport-v1";
41
+ /** Path of the signed Next route the function relays to. */
42
+ export declare const MCP_INTERNAL_PATH = "/api/mcp-internal";
43
+ /** Value of the marker header on every relayed response. Release checks assert it. */
44
+ export declare const MCP_RELAY_MARKER = "netlify-rate-limited-v1";
45
+ /** Largest POST body the relay forwards (tool arguments), in bytes. */
46
+ export declare const MCP_RELAY_MAX_BODY_BYTES: number;
47
+ /** Upstream timeout. Under Netlify's 60 s synchronous function limit, so the 502 is ours. */
48
+ export declare const MCP_RELAY_TIMEOUT_MS = 55000;
49
+ /** Methods the MCP wire uses. Anything else gets a 405. */
50
+ export declare const MCP_RELAY_METHODS: readonly ['GET', 'POST', 'OPTIONS', 'DELETE'];
51
+ export interface McpTransportOptions {
52
+ /**
53
+ * Request header that carries the signature. The relay also stamps it on
54
+ * responses with {@link MCP_RELAY_MARKER}. Must match between the function
55
+ * and the Next routes. Default {@link DEFAULT_MCP_TRANSPORT_HEADER}.
56
+ */
57
+ header?: string;
58
+ /**
59
+ * HMAC message. Must match between the function and the Next routes.
60
+ * Default {@link DEFAULT_MCP_TRANSPORT_LABEL}.
61
+ */
62
+ label?: string;
63
+ /**
64
+ * The HMAC key, or a getter for it. Default: `MCP_TRANSPORT_SECRET`, read on
65
+ * every request (`Netlify.env` when present, else `process.env`).
66
+ */
67
+ secret?: string | (() => string | undefined);
68
+ }
69
+ export type McpRouteHandler = (request: Request) => Response | Promise<Response>;
70
+ /** The four method handlers an MCP route exports (what `createMcpHandler` returns). */
71
+ export interface McpRouteHandlerSet {
72
+ GET: McpRouteHandler;
73
+ POST: McpRouteHandler;
74
+ DELETE: McpRouteHandler;
75
+ OPTIONS: McpRouteHandler;
76
+ }
77
+ export interface ProtectMcpHandlersOptions extends McpTransportOptions {
78
+ /**
79
+ * Refuse unsigned requests. Default: `NODE_ENV === 'production'`, read per
80
+ * request, so `next dev` still answers a local MCP client directly.
81
+ */
82
+ enforce?: boolean | (() => boolean);
83
+ }
84
+ export interface NetlifyMcpRelayOptions extends McpTransportOptions {
85
+ /** The signed Next route, on the same deploy. Default {@link MCP_INTERNAL_PATH}. */
86
+ internalPath?: string;
87
+ /** Error text for the 502 when the upstream fails or times out. */
88
+ timeoutMessage?: string;
89
+ }
90
+ /** The parts of Netlify's function `context` the relay reads. */
91
+ export interface NetlifyRelayContext {
92
+ deploy?: {
93
+ id?: string;
94
+ };
95
+ site?: {
96
+ name?: string;
97
+ url?: string;
98
+ };
99
+ }
100
+ /** The signature a relayed request carries: hex HMAC-SHA256 of `label` under `secret`. */
101
+ export declare function signMcpTransport(secret: string, label?: string): string;
102
+ /**
103
+ * True when the request came through the rate-limited relay: its transport
104
+ * header holds the HMAC of `label` under `MCP_TRANSPORT_SECRET`, compared in
105
+ * constant time. False when the secret is unset.
106
+ */
107
+ export declare function hasMcpTransportAuthorization(request: Request, options?: McpTransportOptions): boolean;
108
+ /**
109
+ * Wrap the `/api/mcp` handlers so, in production, they answer only relayed
110
+ * (signed) requests. The public path is the function's; this closes the Next
111
+ * handler behind it so nothing can skip the rate limit.
112
+ *
113
+ * @example
114
+ * ```ts
115
+ * // app/api/mcp/route.ts
116
+ * export const { POST, GET, DELETE, OPTIONS } = protectMcpHandlers(
117
+ * createMcpHandler({ server: mcpServer, baseUrl: SITE_URL, allowedOrigins: '*' }),
118
+ * )
119
+ * ```
120
+ */
121
+ export declare function protectMcpHandlers(handlers: McpRouteHandlerSet, options?: ProtectMcpHandlersOptions): McpRouteHandlerSet;
122
+ /**
123
+ * Handlers for `/api/mcp-internal`, the relay's target. 403 without a valid
124
+ * signature in every environment, then dispatch by method to the `/api/mcp`
125
+ * handlers.
126
+ *
127
+ * @example
128
+ * ```ts
129
+ * // app/api/mcp-internal/route.ts
130
+ * import * as mcp from '../mcp/route'
131
+ * export const { POST, GET, DELETE, OPTIONS } = createMcpInternalRoute(mcp)
132
+ * ```
133
+ */
134
+ export declare function createMcpInternalRoute(handlers: McpRouteHandlerSet, options?: McpTransportOptions): McpRouteHandlerSet;
135
+ /**
136
+ * The default export for `netlify/functions/mcp.mjs`. Signs the request and
137
+ * relays it to {@link MCP_INTERNAL_PATH} on the same deploy.
138
+ *
139
+ * - 503 when `MCP_TRANSPORT_SECRET` is unset; 405 for methods MCP doesn't use.
140
+ * - Strips `host` and `content-length`, overwrites any client-sent transport
141
+ * header with the real signature.
142
+ * - POST bodies over 64 KB get a 413 and are never forwarded.
143
+ * - `redirect: 'error'` and a 55 s timeout; any upstream failure is a 502.
144
+ * - Responses drop the encoding/length headers fetch already undid, and gain
145
+ * `Cache-Control: no-store` plus the transport header set to
146
+ * {@link MCP_RELAY_MARKER}.
147
+ *
148
+ * The function file must still declare its own literal `config`: Netlify
149
+ * reads `path` and `rateLimit` statically from that file, so they can't come
150
+ * from an import.
151
+ *
152
+ * @example
153
+ * ```js
154
+ * // netlify/functions/mcp.mjs
155
+ * import { createNetlifyMcpRelay } from '@sonordev/site-kit/mcp/transport'
156
+ *
157
+ * export default createNetlifyMcpRelay()
158
+ *
159
+ * export const config = {
160
+ * path: '/api/mcp',
161
+ * rateLimit: { windowSize: 60, windowLimit: 60, aggregateBy: ['ip', 'domain'] },
162
+ * }
163
+ * ```
164
+ */
165
+ export declare function createNetlifyMcpRelay(options?: NetlifyMcpRelayOptions): (request: Request, context: NetlifyRelayContext) => Promise<Response>;
166
+ //# sourceMappingURL=transport.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"transport.d.ts","sourceRoot":"","sources":["../../src/mcp/transport.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAiCG;AASH,4CAA4C;AAC5C,eAAO,MAAM,wBAAwB,yBAAyB,CAAA;AAE9D,+EAA+E;AAC/E,eAAO,MAAM,4BAA4B,yBAAyB,CAAA;AAElE,6EAA6E;AAC7E,eAAO,MAAM,2BAA2B,0BAA0B,CAAA;AAElE,4DAA4D;AAC5D,eAAO,MAAM,iBAAiB,sBAAsB,CAAA;AAEpD,sFAAsF;AACtF,eAAO,MAAM,gBAAgB,4BAA4B,CAAA;AAEzD,uEAAuE;AACvE,eAAO,MAAM,wBAAwB,QAAY,CAAA;AAEjD,6FAA6F;AAC7F,eAAO,MAAM,oBAAoB,QAAS,CAAA;AAE1C,2DAA2D;AAC3D,eAAO,MAAM,iBAAiB,YAAI,KAAK,EAAE,MAAM,EAAE,SAAS,EAAE,QAAQ,CAAU,CAAA;AAM9E,MAAM,WAAW,mBAAmB;IAClC;;;;OAIG;IACH,MAAM,CAAC,EAAE,MAAM,CAAA;IACf;;;OAGG;IACH,KAAK,CAAC,EAAE,MAAM,CAAA;IACd;;;OAGG;IACH,MAAM,CAAC,EAAE,MAAM,GAAG,CAAC,MAAM,MAAM,GAAG,SAAS,CAAC,CAAA;CAC7C;AAED,MAAM,MAAM,eAAe,GAAG,CAAC,OAAO,EAAE,OAAO,KAAK,QAAQ,GAAG,OAAO,CAAC,QAAQ,CAAC,CAAA;AAEhF,uFAAuF;AACvF,MAAM,WAAW,kBAAkB;IACjC,GAAG,EAAE,eAAe,CAAA;IACpB,IAAI,EAAE,eAAe,CAAA;IACrB,MAAM,EAAE,eAAe,CAAA;IACvB,OAAO,EAAE,eAAe,CAAA;CACzB;AAED,MAAM,WAAW,yBAA0B,SAAQ,mBAAmB;IACpE;;;OAGG;IACH,OAAO,CAAC,EAAE,OAAO,GAAG,CAAC,MAAM,OAAO,CAAC,CAAA;CACpC;AAED,MAAM,WAAW,sBAAuB,SAAQ,mBAAmB;IACjE,oFAAoF;IACpF,YAAY,CAAC,EAAE,MAAM,CAAA;IACrB,mEAAmE;IACnE,cAAc,CAAC,EAAE,MAAM,CAAA;CACxB;AAED,iEAAiE;AACjE,MAAM,WAAW,mBAAmB;IAClC,MAAM,CAAC,EAAE;QAAE,EAAE,CAAC,EAAE,MAAM,CAAA;KAAE,CAAA;IACxB,IAAI,CAAC,EAAE;QAAE,IAAI,CAAC,EAAE,MAAM,CAAC;QAAC,GAAG,CAAC,EAAE,MAAM,CAAA;KAAE,CAAA;CACvC;AA6BD,0FAA0F;AAC1F,wBAAgB,gBAAgB,CAAC,MAAM,EAAE,MAAM,EAAE,KAAK,GAAE,MAAoC,GAAG,MAAM,CAEpG;AAUD;;;;GAIG;AACH,wBAAgB,4BAA4B,CAAC,OAAO,EAAE,OAAO,EAAE,OAAO,GAAE,mBAAwB,GAAG,OAAO,CAEzG;AAYD;;;;;;;;;;;;GAYG;AACH,wBAAgB,kBAAkB,CAChC,QAAQ,EAAE,kBAAkB,EAC5B,OAAO,GAAE,yBAA8B,GACtC,kBAAkB,CAgBpB;AAED;;;;;;;;;;;GAWG;AACH,wBAAgB,sBAAsB,CACpC,QAAQ,EAAE,kBAAkB,EAC5B,OAAO,GAAE,mBAAwB,GAChC,kBAAkB,CAkBpB;AAMD;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA6BG;AACH,wBAAgB,qBAAqB,CACnC,OAAO,GAAE,sBAA2B,GACnC,CAAC,OAAO,EAAE,OAAO,EAAE,OAAO,EAAE,mBAAmB,KAAK,OAAO,CAAC,QAAQ,CAAC,CAmDvE"}
@@ -0,0 +1,137 @@
1
+ 'use strict';
2
+
3
+ var chunkYMZNEDFP_js = require('../chunk-YMZNEDFP.js');
4
+ require('../chunk-SNAQBZPT.js');
5
+ var crypto = require('crypto');
6
+
7
+ var MCP_TRANSPORT_SECRET_ENV = "MCP_TRANSPORT_SECRET";
8
+ var DEFAULT_MCP_TRANSPORT_HEADER = "x-site-mcp-transport";
9
+ var DEFAULT_MCP_TRANSPORT_LABEL = "site-mcp-transport-v1";
10
+ var MCP_INTERNAL_PATH = "/api/mcp-internal";
11
+ var MCP_RELAY_MARKER = "netlify-rate-limited-v1";
12
+ var MCP_RELAY_MAX_BODY_BYTES = 64 * 1024;
13
+ var MCP_RELAY_TIMEOUT_MS = 55e3;
14
+ var MCP_RELAY_METHODS = ["GET", "POST", "OPTIONS", "DELETE"];
15
+ function resolveTransport(options = {}) {
16
+ const header = (options.header ?? DEFAULT_MCP_TRANSPORT_HEADER).toLowerCase();
17
+ const label = options.label ?? DEFAULT_MCP_TRANSPORT_LABEL;
18
+ if (!/^[a-z0-9-]+$/.test(header)) {
19
+ throw new TypeError(`@sonordev/site-kit/mcp/transport: invalid header name ${JSON.stringify(options.header)}`);
20
+ }
21
+ if (!label) throw new TypeError("@sonordev/site-kit/mcp/transport: label must not be empty");
22
+ return { header, label, secret: options.secret };
23
+ }
24
+ function readSecret(secret) {
25
+ if (typeof secret === "function") return secret();
26
+ if (secret !== void 0) return secret;
27
+ const netlify = globalThis.Netlify;
28
+ return netlify?.env?.get(MCP_TRANSPORT_SECRET_ENV) || process.env[MCP_TRANSPORT_SECRET_ENV];
29
+ }
30
+ function signMcpTransport(secret, label = DEFAULT_MCP_TRANSPORT_LABEL) {
31
+ return crypto.createHmac("sha256", secret).update(label).digest("hex");
32
+ }
33
+ function verify(request, transport) {
34
+ const key = readSecret(transport.secret);
35
+ const supplied = request.headers.get(transport.header) || "";
36
+ if (!key || !/^[a-f0-9]{64}$/.test(supplied)) return false;
37
+ const expected = crypto.createHmac("sha256", key).update(transport.label).digest();
38
+ return crypto.timingSafeEqual(expected, Buffer.from(supplied, "hex"));
39
+ }
40
+ function hasMcpTransportAuthorization(request, options = {}) {
41
+ return verify(request, resolveTransport(options));
42
+ }
43
+ function isEnforced(enforce) {
44
+ if (typeof enforce === "function") return enforce();
45
+ if (enforce !== void 0) return enforce;
46
+ return process.env.NODE_ENV === "production";
47
+ }
48
+ function protectMcpHandlers(handlers, options = {}) {
49
+ const transport = resolveTransport(options);
50
+ const protect = (handler) => (request) => {
51
+ if (isEnforced(options.enforce) && !verify(request, transport)) {
52
+ return Response.json({ error: "Use the public MCP endpoint" }, { status: 403 });
53
+ }
54
+ return handler(request);
55
+ };
56
+ return {
57
+ GET: protect(handlers.GET),
58
+ POST: protect(handlers.POST),
59
+ DELETE: protect(handlers.DELETE),
60
+ OPTIONS: protect(handlers.OPTIONS)
61
+ };
62
+ }
63
+ function createMcpInternalRoute(handlers, options = {}) {
64
+ const transport = resolveTransport(options);
65
+ const relay = (request) => {
66
+ if (!verify(request, transport)) return Response.json({ error: "Forbidden" }, { status: 403 });
67
+ switch (request.method) {
68
+ case "GET":
69
+ return handlers.GET(request);
70
+ case "POST":
71
+ return handlers.POST(request);
72
+ case "DELETE":
73
+ return handlers.DELETE(request);
74
+ case "OPTIONS":
75
+ return handlers.OPTIONS(request);
76
+ default:
77
+ return new Response(null, { status: 405 });
78
+ }
79
+ };
80
+ return { GET: relay, POST: relay, DELETE: relay, OPTIONS: relay };
81
+ }
82
+ function createNetlifyMcpRelay(options = {}) {
83
+ const transport = resolveTransport(options);
84
+ const internalPath = options.internalPath ?? MCP_INTERNAL_PATH;
85
+ const timeoutMessage = options.timeoutMessage ?? "MCP request timed out. Try again.";
86
+ const unavailable = () => Response.json({ error: "MCP is temporarily unavailable" }, { status: 503 });
87
+ return async function relayMcp(request, context) {
88
+ const key = readSecret(transport.secret);
89
+ if (!key) return unavailable();
90
+ if (!MCP_RELAY_METHODS.includes(request.method)) return new Response(null, { status: 405 });
91
+ const base = context?.deploy?.id && context.site?.name ? `https://${context.deploy.id}--${context.site.name}.netlify.app` : context?.site?.url;
92
+ if (!base) return unavailable();
93
+ const target = new URL(internalPath, base);
94
+ const headers = new Headers(request.headers);
95
+ headers.delete("host");
96
+ headers.delete("content-length");
97
+ headers.set(transport.header, signMcpTransport(key, transport.label));
98
+ let body;
99
+ if (request.method === "POST") {
100
+ const bytes = await chunkYMZNEDFP_js.readCappedBody(request, MCP_RELAY_MAX_BODY_BYTES);
101
+ if (!bytes) return Response.json({ error: "Tool arguments are too large" }, { status: 413 });
102
+ body = bytes;
103
+ }
104
+ try {
105
+ const response = await fetch(target, {
106
+ method: request.method,
107
+ headers,
108
+ body,
109
+ redirect: "error",
110
+ signal: AbortSignal.timeout(MCP_RELAY_TIMEOUT_MS)
111
+ });
112
+ const outgoing = new Headers(response.headers);
113
+ for (const name of ["content-encoding", "content-length", "transfer-encoding"]) outgoing.delete(name);
114
+ outgoing.set("Cache-Control", "no-store");
115
+ outgoing.set(transport.header, MCP_RELAY_MARKER);
116
+ return new Response(response.body, { status: response.status, headers: outgoing });
117
+ } catch {
118
+ return Response.json({ error: timeoutMessage }, { status: 502 });
119
+ }
120
+ };
121
+ }
122
+
123
+ exports.DEFAULT_MCP_TRANSPORT_HEADER = DEFAULT_MCP_TRANSPORT_HEADER;
124
+ exports.DEFAULT_MCP_TRANSPORT_LABEL = DEFAULT_MCP_TRANSPORT_LABEL;
125
+ exports.MCP_INTERNAL_PATH = MCP_INTERNAL_PATH;
126
+ exports.MCP_RELAY_MARKER = MCP_RELAY_MARKER;
127
+ exports.MCP_RELAY_MAX_BODY_BYTES = MCP_RELAY_MAX_BODY_BYTES;
128
+ exports.MCP_RELAY_METHODS = MCP_RELAY_METHODS;
129
+ exports.MCP_RELAY_TIMEOUT_MS = MCP_RELAY_TIMEOUT_MS;
130
+ exports.MCP_TRANSPORT_SECRET_ENV = MCP_TRANSPORT_SECRET_ENV;
131
+ exports.createMcpInternalRoute = createMcpInternalRoute;
132
+ exports.createNetlifyMcpRelay = createNetlifyMcpRelay;
133
+ exports.hasMcpTransportAuthorization = hasMcpTransportAuthorization;
134
+ exports.protectMcpHandlers = protectMcpHandlers;
135
+ exports.signMcpTransport = signMcpTransport;
136
+ //# sourceMappingURL=transport.js.map
137
+ //# sourceMappingURL=transport.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../../src/mcp/transport.ts"],"names":["createHmac","timingSafeEqual","readCappedBody"],"mappings":"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","file":"transport.js","sourcesContent":["/**\n * @sonordev/site-kit/mcp/transport — the rate-limited public MCP relay\n *\n * Netlify rate-limits a native function (declarative `config.rateLimit`), never\n * a Next.js route handler. So a public MCP endpoint on Netlify takes this path:\n *\n * agent ─► /api/mcp netlify/functions/mcp.mjs, rate-limited\n * │ createNetlifyMcpRelay: signs the request\n * │ (HMAC-SHA256 of MCP_TRANSPORT_SECRET over a label)\n * ▼\n * https://<deploy-id>--<site>.netlify.app/api/mcp-internal\n * │ createMcpInternalRoute: 403 unless signed,\n * ▼ then dispatches to the /api/mcp handlers\n * app/api/mcp/route.ts protectMcpHandlers(createMcpHandler(...))\n *\n * In production the function owns /api/mcp, and the Next route behind it\n * refuses unsigned calls too, so no request reaches a tool without passing\n * the rate limit. The relay targets the deploy permalink, so a request is\n * answered by the same immutable deploy that received it, and the base URL\n * comes from the platform (`context`), never from a client header.\n *\n * Single source of truth for the relay. It replaced per-site copies in\n * upforge-nextjs and wirsch-law-nextjs (`lib/mcp/transport-auth.ts`, the\n * `protect()` wrapper, the `/api/mcp-internal` relay and the function body);\n * don't fork it back into a site.\n *\n * Node only (`crypto`), and deliberately not re-exported from the\n * `@sonordev/site-kit/mcp` barrel so that barrel stays runtime-neutral.\n * Nothing here imports `server-only`: the Netlify function runs in plain Node,\n * where `server-only` throws on import.\n *\n * Setup: `MCP_TRANSPORT_SECRET` (`openssl rand -hex 32`) in every deploy\n * context, Functions scope. See the module README for the three files.\n */\n\nimport { createHmac, timingSafeEqual } from 'crypto'\nimport { readCappedBody } from '../shared/request-guards'\n\n// ============================================\n// Constants\n// ============================================\n\n/** Env var holding the relay's HMAC key. */\nexport const MCP_TRANSPORT_SECRET_ENV = 'MCP_TRANSPORT_SECRET'\n\n/** Default request header carrying the signature (and the response marker). */\nexport const DEFAULT_MCP_TRANSPORT_HEADER = 'x-site-mcp-transport'\n\n/** Default HMAC message. Changing it invalidates every signature at once. */\nexport const DEFAULT_MCP_TRANSPORT_LABEL = 'site-mcp-transport-v1'\n\n/** Path of the signed Next route the function relays to. */\nexport const MCP_INTERNAL_PATH = '/api/mcp-internal'\n\n/** Value of the marker header on every relayed response. Release checks assert it. */\nexport const MCP_RELAY_MARKER = 'netlify-rate-limited-v1'\n\n/** Largest POST body the relay forwards (tool arguments), in bytes. */\nexport const MCP_RELAY_MAX_BODY_BYTES = 64 * 1024\n\n/** Upstream timeout. Under Netlify's 60 s synchronous function limit, so the 502 is ours. */\nexport const MCP_RELAY_TIMEOUT_MS = 55_000\n\n/** Methods the MCP wire uses. Anything else gets a 405. */\nexport const MCP_RELAY_METHODS = ['GET', 'POST', 'OPTIONS', 'DELETE'] as const\n\n// ============================================\n// Types\n// ============================================\n\nexport interface McpTransportOptions {\n /**\n * Request header that carries the signature. The relay also stamps it on\n * responses with {@link MCP_RELAY_MARKER}. Must match between the function\n * and the Next routes. Default {@link DEFAULT_MCP_TRANSPORT_HEADER}.\n */\n header?: string\n /**\n * HMAC message. Must match between the function and the Next routes.\n * Default {@link DEFAULT_MCP_TRANSPORT_LABEL}.\n */\n label?: string\n /**\n * The HMAC key, or a getter for it. Default: `MCP_TRANSPORT_SECRET`, read on\n * every request (`Netlify.env` when present, else `process.env`).\n */\n secret?: string | (() => string | undefined)\n}\n\nexport type McpRouteHandler = (request: Request) => Response | Promise<Response>\n\n/** The four method handlers an MCP route exports (what `createMcpHandler` returns). */\nexport interface McpRouteHandlerSet {\n GET: McpRouteHandler\n POST: McpRouteHandler\n DELETE: McpRouteHandler\n OPTIONS: McpRouteHandler\n}\n\nexport interface ProtectMcpHandlersOptions extends McpTransportOptions {\n /**\n * Refuse unsigned requests. Default: `NODE_ENV === 'production'`, read per\n * request, so `next dev` still answers a local MCP client directly.\n */\n enforce?: boolean | (() => boolean)\n}\n\nexport interface NetlifyMcpRelayOptions extends McpTransportOptions {\n /** The signed Next route, on the same deploy. Default {@link MCP_INTERNAL_PATH}. */\n internalPath?: string\n /** Error text for the 502 when the upstream fails or times out. */\n timeoutMessage?: string\n}\n\n/** The parts of Netlify's function `context` the relay reads. */\nexport interface NetlifyRelayContext {\n deploy?: { id?: string }\n site?: { name?: string; url?: string }\n}\n\ninterface ResolvedTransport {\n header: string\n label: string\n secret: McpTransportOptions['secret']\n}\n\n// ============================================\n// Signing\n// ============================================\n\nfunction resolveTransport(options: McpTransportOptions = {}): ResolvedTransport {\n const header = (options.header ?? DEFAULT_MCP_TRANSPORT_HEADER).toLowerCase()\n const label = options.label ?? DEFAULT_MCP_TRANSPORT_LABEL\n if (!/^[a-z0-9-]+$/.test(header)) {\n throw new TypeError(`@sonordev/site-kit/mcp/transport: invalid header name ${JSON.stringify(options.header)}`)\n }\n if (!label) throw new TypeError('@sonordev/site-kit/mcp/transport: label must not be empty')\n return { header, label, secret: options.secret }\n}\n\nfunction readSecret(secret: McpTransportOptions['secret']): string | undefined {\n if (typeof secret === 'function') return secret()\n if (secret !== undefined) return secret\n const netlify = (globalThis as { Netlify?: { env?: { get(name: string): string | undefined } } }).Netlify\n return netlify?.env?.get(MCP_TRANSPORT_SECRET_ENV) || process.env[MCP_TRANSPORT_SECRET_ENV]\n}\n\n/** The signature a relayed request carries: hex HMAC-SHA256 of `label` under `secret`. */\nexport function signMcpTransport(secret: string, label: string = DEFAULT_MCP_TRANSPORT_LABEL): string {\n return createHmac('sha256', secret).update(label).digest('hex')\n}\n\nfunction verify(request: Request, transport: ResolvedTransport): boolean {\n const key = readSecret(transport.secret)\n const supplied = request.headers.get(transport.header) || ''\n if (!key || !/^[a-f0-9]{64}$/.test(supplied)) return false\n const expected = createHmac('sha256', key).update(transport.label).digest()\n return timingSafeEqual(expected, Buffer.from(supplied, 'hex'))\n}\n\n/**\n * True when the request came through the rate-limited relay: its transport\n * header holds the HMAC of `label` under `MCP_TRANSPORT_SECRET`, compared in\n * constant time. False when the secret is unset.\n */\nexport function hasMcpTransportAuthorization(request: Request, options: McpTransportOptions = {}): boolean {\n return verify(request, resolveTransport(options))\n}\n\n// ============================================\n// Next.js routes\n// ============================================\n\nfunction isEnforced(enforce: ProtectMcpHandlersOptions['enforce']): boolean {\n if (typeof enforce === 'function') return enforce()\n if (enforce !== undefined) return enforce\n return process.env.NODE_ENV === 'production'\n}\n\n/**\n * Wrap the `/api/mcp` handlers so, in production, they answer only relayed\n * (signed) requests. The public path is the function's; this closes the Next\n * handler behind it so nothing can skip the rate limit.\n *\n * @example\n * ```ts\n * // app/api/mcp/route.ts\n * export const { POST, GET, DELETE, OPTIONS } = protectMcpHandlers(\n * createMcpHandler({ server: mcpServer, baseUrl: SITE_URL, allowedOrigins: '*' }),\n * )\n * ```\n */\nexport function protectMcpHandlers(\n handlers: McpRouteHandlerSet,\n options: ProtectMcpHandlersOptions = {},\n): McpRouteHandlerSet {\n const transport = resolveTransport(options)\n const protect =\n (handler: McpRouteHandler): McpRouteHandler =>\n (request) => {\n if (isEnforced(options.enforce) && !verify(request, transport)) {\n return Response.json({ error: 'Use the public MCP endpoint' }, { status: 403 })\n }\n return handler(request)\n }\n return {\n GET: protect(handlers.GET),\n POST: protect(handlers.POST),\n DELETE: protect(handlers.DELETE),\n OPTIONS: protect(handlers.OPTIONS),\n }\n}\n\n/**\n * Handlers for `/api/mcp-internal`, the relay's target. 403 without a valid\n * signature in every environment, then dispatch by method to the `/api/mcp`\n * handlers.\n *\n * @example\n * ```ts\n * // app/api/mcp-internal/route.ts\n * import * as mcp from '../mcp/route'\n * export const { POST, GET, DELETE, OPTIONS } = createMcpInternalRoute(mcp)\n * ```\n */\nexport function createMcpInternalRoute(\n handlers: McpRouteHandlerSet,\n options: McpTransportOptions = {},\n): McpRouteHandlerSet {\n const transport = resolveTransport(options)\n const relay: McpRouteHandler = (request) => {\n if (!verify(request, transport)) return Response.json({ error: 'Forbidden' }, { status: 403 })\n switch (request.method) {\n case 'GET':\n return handlers.GET(request)\n case 'POST':\n return handlers.POST(request)\n case 'DELETE':\n return handlers.DELETE(request)\n case 'OPTIONS':\n return handlers.OPTIONS(request)\n default:\n return new Response(null, { status: 405 })\n }\n }\n return { GET: relay, POST: relay, DELETE: relay, OPTIONS: relay }\n}\n\n// ============================================\n// Netlify function\n// ============================================\n\n/**\n * The default export for `netlify/functions/mcp.mjs`. Signs the request and\n * relays it to {@link MCP_INTERNAL_PATH} on the same deploy.\n *\n * - 503 when `MCP_TRANSPORT_SECRET` is unset; 405 for methods MCP doesn't use.\n * - Strips `host` and `content-length`, overwrites any client-sent transport\n * header with the real signature.\n * - POST bodies over 64 KB get a 413 and are never forwarded.\n * - `redirect: 'error'` and a 55 s timeout; any upstream failure is a 502.\n * - Responses drop the encoding/length headers fetch already undid, and gain\n * `Cache-Control: no-store` plus the transport header set to\n * {@link MCP_RELAY_MARKER}.\n *\n * The function file must still declare its own literal `config`: Netlify\n * reads `path` and `rateLimit` statically from that file, so they can't come\n * from an import.\n *\n * @example\n * ```js\n * // netlify/functions/mcp.mjs\n * import { createNetlifyMcpRelay } from '@sonordev/site-kit/mcp/transport'\n *\n * export default createNetlifyMcpRelay()\n *\n * export const config = {\n * path: '/api/mcp',\n * rateLimit: { windowSize: 60, windowLimit: 60, aggregateBy: ['ip', 'domain'] },\n * }\n * ```\n */\nexport function createNetlifyMcpRelay(\n options: NetlifyMcpRelayOptions = {},\n): (request: Request, context: NetlifyRelayContext) => Promise<Response> {\n const transport = resolveTransport(options)\n const internalPath = options.internalPath ?? MCP_INTERNAL_PATH\n const timeoutMessage = options.timeoutMessage ?? 'MCP request timed out. Try again.'\n const unavailable = () => Response.json({ error: 'MCP is temporarily unavailable' }, { status: 503 })\n\n return async function relayMcp(request, context) {\n const key = readSecret(transport.secret)\n if (!key) return unavailable()\n if (!(MCP_RELAY_METHODS as readonly string[]).includes(request.method)) return new Response(null, { status: 405 })\n\n // context is platform-provided, never a client-controlled forwarding URL.\n // The deploy permalink keeps the relay on the deploy that took the call.\n const base =\n context?.deploy?.id && context.site?.name\n ? `https://${context.deploy.id}--${context.site.name}.netlify.app`\n : context?.site?.url\n if (!base) return unavailable()\n const target = new URL(internalPath, base)\n\n const headers = new Headers(request.headers)\n headers.delete('host')\n headers.delete('content-length')\n // set, not append: overwrite anything a client sent under this name.\n headers.set(transport.header, signMcpTransport(key, transport.label))\n\n let body: Uint8Array<ArrayBuffer> | undefined\n if (request.method === 'POST') {\n const bytes = await readCappedBody(request, MCP_RELAY_MAX_BODY_BYTES)\n if (!bytes) return Response.json({ error: 'Tool arguments are too large' }, { status: 413 })\n body = bytes\n }\n\n try {\n const response = await fetch(target, {\n method: request.method,\n headers,\n body,\n redirect: 'error',\n signal: AbortSignal.timeout(MCP_RELAY_TIMEOUT_MS),\n })\n const outgoing = new Headers(response.headers)\n // fetch decodes the upstream body; its compression/length headers no longer apply.\n for (const name of ['content-encoding', 'content-length', 'transfer-encoding']) outgoing.delete(name)\n outgoing.set('Cache-Control', 'no-store')\n outgoing.set(transport.header, MCP_RELAY_MARKER)\n return new Response(response.body, { status: response.status, headers: outgoing })\n } catch {\n return Response.json({ error: timeoutMessage }, { status: 502 })\n }\n }\n}\n"]}
@@ -0,0 +1,123 @@
1
+ import { readCappedBody } from '../chunk-Z2URIOKG.mjs';
2
+ import '../chunk-BEXTDHO4.mjs';
3
+ import { createHmac, timingSafeEqual } from 'crypto';
4
+
5
+ var MCP_TRANSPORT_SECRET_ENV = "MCP_TRANSPORT_SECRET";
6
+ var DEFAULT_MCP_TRANSPORT_HEADER = "x-site-mcp-transport";
7
+ var DEFAULT_MCP_TRANSPORT_LABEL = "site-mcp-transport-v1";
8
+ var MCP_INTERNAL_PATH = "/api/mcp-internal";
9
+ var MCP_RELAY_MARKER = "netlify-rate-limited-v1";
10
+ var MCP_RELAY_MAX_BODY_BYTES = 64 * 1024;
11
+ var MCP_RELAY_TIMEOUT_MS = 55e3;
12
+ var MCP_RELAY_METHODS = ["GET", "POST", "OPTIONS", "DELETE"];
13
+ function resolveTransport(options = {}) {
14
+ const header = (options.header ?? DEFAULT_MCP_TRANSPORT_HEADER).toLowerCase();
15
+ const label = options.label ?? DEFAULT_MCP_TRANSPORT_LABEL;
16
+ if (!/^[a-z0-9-]+$/.test(header)) {
17
+ throw new TypeError(`@sonordev/site-kit/mcp/transport: invalid header name ${JSON.stringify(options.header)}`);
18
+ }
19
+ if (!label) throw new TypeError("@sonordev/site-kit/mcp/transport: label must not be empty");
20
+ return { header, label, secret: options.secret };
21
+ }
22
+ function readSecret(secret) {
23
+ if (typeof secret === "function") return secret();
24
+ if (secret !== void 0) return secret;
25
+ const netlify = globalThis.Netlify;
26
+ return netlify?.env?.get(MCP_TRANSPORT_SECRET_ENV) || process.env[MCP_TRANSPORT_SECRET_ENV];
27
+ }
28
+ function signMcpTransport(secret, label = DEFAULT_MCP_TRANSPORT_LABEL) {
29
+ return createHmac("sha256", secret).update(label).digest("hex");
30
+ }
31
+ function verify(request, transport) {
32
+ const key = readSecret(transport.secret);
33
+ const supplied = request.headers.get(transport.header) || "";
34
+ if (!key || !/^[a-f0-9]{64}$/.test(supplied)) return false;
35
+ const expected = createHmac("sha256", key).update(transport.label).digest();
36
+ return timingSafeEqual(expected, Buffer.from(supplied, "hex"));
37
+ }
38
+ function hasMcpTransportAuthorization(request, options = {}) {
39
+ return verify(request, resolveTransport(options));
40
+ }
41
+ function isEnforced(enforce) {
42
+ if (typeof enforce === "function") return enforce();
43
+ if (enforce !== void 0) return enforce;
44
+ return process.env.NODE_ENV === "production";
45
+ }
46
+ function protectMcpHandlers(handlers, options = {}) {
47
+ const transport = resolveTransport(options);
48
+ const protect = (handler) => (request) => {
49
+ if (isEnforced(options.enforce) && !verify(request, transport)) {
50
+ return Response.json({ error: "Use the public MCP endpoint" }, { status: 403 });
51
+ }
52
+ return handler(request);
53
+ };
54
+ return {
55
+ GET: protect(handlers.GET),
56
+ POST: protect(handlers.POST),
57
+ DELETE: protect(handlers.DELETE),
58
+ OPTIONS: protect(handlers.OPTIONS)
59
+ };
60
+ }
61
+ function createMcpInternalRoute(handlers, options = {}) {
62
+ const transport = resolveTransport(options);
63
+ const relay = (request) => {
64
+ if (!verify(request, transport)) return Response.json({ error: "Forbidden" }, { status: 403 });
65
+ switch (request.method) {
66
+ case "GET":
67
+ return handlers.GET(request);
68
+ case "POST":
69
+ return handlers.POST(request);
70
+ case "DELETE":
71
+ return handlers.DELETE(request);
72
+ case "OPTIONS":
73
+ return handlers.OPTIONS(request);
74
+ default:
75
+ return new Response(null, { status: 405 });
76
+ }
77
+ };
78
+ return { GET: relay, POST: relay, DELETE: relay, OPTIONS: relay };
79
+ }
80
+ function createNetlifyMcpRelay(options = {}) {
81
+ const transport = resolveTransport(options);
82
+ const internalPath = options.internalPath ?? MCP_INTERNAL_PATH;
83
+ const timeoutMessage = options.timeoutMessage ?? "MCP request timed out. Try again.";
84
+ const unavailable = () => Response.json({ error: "MCP is temporarily unavailable" }, { status: 503 });
85
+ return async function relayMcp(request, context) {
86
+ const key = readSecret(transport.secret);
87
+ if (!key) return unavailable();
88
+ if (!MCP_RELAY_METHODS.includes(request.method)) return new Response(null, { status: 405 });
89
+ const base = context?.deploy?.id && context.site?.name ? `https://${context.deploy.id}--${context.site.name}.netlify.app` : context?.site?.url;
90
+ if (!base) return unavailable();
91
+ const target = new URL(internalPath, base);
92
+ const headers = new Headers(request.headers);
93
+ headers.delete("host");
94
+ headers.delete("content-length");
95
+ headers.set(transport.header, signMcpTransport(key, transport.label));
96
+ let body;
97
+ if (request.method === "POST") {
98
+ const bytes = await readCappedBody(request, MCP_RELAY_MAX_BODY_BYTES);
99
+ if (!bytes) return Response.json({ error: "Tool arguments are too large" }, { status: 413 });
100
+ body = bytes;
101
+ }
102
+ try {
103
+ const response = await fetch(target, {
104
+ method: request.method,
105
+ headers,
106
+ body,
107
+ redirect: "error",
108
+ signal: AbortSignal.timeout(MCP_RELAY_TIMEOUT_MS)
109
+ });
110
+ const outgoing = new Headers(response.headers);
111
+ for (const name of ["content-encoding", "content-length", "transfer-encoding"]) outgoing.delete(name);
112
+ outgoing.set("Cache-Control", "no-store");
113
+ outgoing.set(transport.header, MCP_RELAY_MARKER);
114
+ return new Response(response.body, { status: response.status, headers: outgoing });
115
+ } catch {
116
+ return Response.json({ error: timeoutMessage }, { status: 502 });
117
+ }
118
+ };
119
+ }
120
+
121
+ export { DEFAULT_MCP_TRANSPORT_HEADER, DEFAULT_MCP_TRANSPORT_LABEL, MCP_INTERNAL_PATH, MCP_RELAY_MARKER, MCP_RELAY_MAX_BODY_BYTES, MCP_RELAY_METHODS, MCP_RELAY_TIMEOUT_MS, MCP_TRANSPORT_SECRET_ENV, createMcpInternalRoute, createNetlifyMcpRelay, hasMcpTransportAuthorization, protectMcpHandlers, signMcpTransport };
122
+ //# sourceMappingURL=transport.mjs.map
123
+ //# sourceMappingURL=transport.mjs.map