@redocly/realm 0.136.0-next.0 → 0.136.0-next.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (83) hide show
  1. package/CHANGELOG.md +30 -0
  2. package/dist/client/app/hooks/catalog/useCatalogViewMode.js +1 -1
  3. package/dist/client/app/hooks/catalog/useFetchCatalogEntities.js +1 -1
  4. package/dist/client/app/hooks/catalog/useFetchCatalogEntitiesRelations.js +1 -1
  5. package/dist/client/app/hooks/catalog/useSearchTracker.d.ts +2 -5
  6. package/dist/client/app/hooks/catalog/useSearchTracker.js +1 -1
  7. package/dist/server/api-routes/execute-api-route.js +1 -1
  8. package/dist/server/api-routes/helpers/enhance-context.d.ts +3 -1
  9. package/dist/server/api-routes/helpers/enhance-context.js +1 -1
  10. package/dist/server/api-routes/helpers/is-mock-server.d.ts +2 -0
  11. package/dist/server/api-routes/helpers/is-mock-server.js +1 -0
  12. package/dist/server/api-routes/mock-server/kv-state-store.d.ts +3 -0
  13. package/dist/server/api-routes/mock-server/kv-state-store.js +1 -0
  14. package/dist/server/api-routes/mock-server/mock-js-executor.d.ts +3 -0
  15. package/dist/server/api-routes/mock-server/mock-js-executor.js +13 -0
  16. package/dist/server/api-routes/run-api-routes-worker.js +1 -1
  17. package/dist/server/config/env-config.d.ts +4 -0
  18. package/dist/server/config/env-schema.d.ts +10 -0
  19. package/dist/server/config/env-schemas/catalog.d.ts +3 -0
  20. package/dist/server/config/env-schemas/catalog.js +1 -1
  21. package/dist/server/config/env-schemas/feature-flags.d.ts +4 -0
  22. package/dist/server/config/env-schemas/feature-flags.js +1 -1
  23. package/dist/server/plugins/catalog-entities/database/catalog-entities-diff-publisher.d.ts +27 -0
  24. package/dist/server/plugins/catalog-entities/database/catalog-entities-diff-publisher.js +12 -0
  25. package/dist/server/plugins/catalog-entities/database/catalog-entities-publisher.js +1 -12
  26. package/dist/server/plugins/catalog-entities/database/catalog-entities-revisions-pruner.d.ts +22 -0
  27. package/dist/server/plugins/catalog-entities/database/catalog-entities-revisions-pruner.js +62 -0
  28. package/dist/server/plugins/catalog-entities/database/consts.d.ts +5 -0
  29. package/dist/server/plugins/catalog-entities/database/consts.js +1 -1
  30. package/dist/server/plugins/catalog-entities/database/repositories/entities/entities-write-repository.js +1 -1
  31. package/dist/server/plugins/catalog-entities/extensions/extractors/api-description/base.d.ts +4 -1
  32. package/dist/server/plugins/catalog-entities/extensions/extractors/api-description/base.js +1 -1
  33. package/dist/server/plugins/catalog-entities/plugin.js +1 -1
  34. package/dist/server/plugins/markdown/markdoc/plugins/utils.d.ts +5 -0
  35. package/dist/server/plugins/markdown/markdoc/plugins/utils.js +1 -1
  36. package/dist/server/plugins/markdown/runtime-transform.js +1 -1
  37. package/dist/server/plugins/mcp/auth/auth-handlers.d.ts +2 -0
  38. package/dist/server/plugins/mcp/auth/auth-handlers.js +1 -1
  39. package/dist/server/plugins/mcp/codemode/constants.d.ts +3 -0
  40. package/dist/server/plugins/mcp/codemode/constants.js +1 -1
  41. package/dist/server/plugins/mcp/codemode/sandbox/create-sandbox-setup.d.ts +10 -0
  42. package/dist/server/plugins/mcp/codemode/sandbox/create-sandbox-setup.js +11 -0
  43. package/dist/server/plugins/mcp/codemode/sandbox/polyfills.d.ts +2 -0
  44. package/dist/server/plugins/mcp/codemode/sandbox/{sandbox-polyfills.js → polyfills.js} +7 -8
  45. package/dist/server/plugins/mcp/codemode/tools/execute.js +1 -1
  46. package/dist/server/plugins/mcp/codemode/types.d.ts +0 -8
  47. package/dist/server/sandbox/constants/host-functions.d.ts +3 -0
  48. package/dist/server/sandbox/constants/host-functions.js +1 -0
  49. package/dist/server/sandbox/constants/sandbox.d.ts +6 -0
  50. package/dist/server/sandbox/constants/sandbox.js +1 -0
  51. package/dist/server/sandbox/sandbox.d.ts +8 -0
  52. package/dist/server/sandbox/sandbox.js +3 -0
  53. package/dist/server/sandbox/types.d.ts +24 -0
  54. package/dist/server/sandbox/types.js +0 -0
  55. package/dist/server/sandbox/utils/eval-guest-code.d.ts +15 -0
  56. package/dist/server/sandbox/utils/eval-guest-code.js +1 -0
  57. package/dist/server/sandbox/utils/execution-budget.d.ts +11 -0
  58. package/dist/server/sandbox/utils/execution-budget.js +1 -0
  59. package/dist/server/sandbox/utils/setup-host-functions.d.ts +18 -0
  60. package/dist/server/sandbox/utils/setup-host-functions.js +5 -0
  61. package/dist/server/store.js +1 -1
  62. package/dist/server/utils/fetch-with-client.d.ts +10 -0
  63. package/dist/server/utils/fetch-with-client.js +1 -0
  64. package/dist/server/utils/sandbox-context.d.ts +18 -0
  65. package/dist/server/utils/sandbox-context.js +1 -0
  66. package/dist/server/web-server/middleware/apiKeyMiddleware.js +1 -1
  67. package/dist/server/web-server/middleware/catalogAuthMiddleware.js +1 -1
  68. package/dist/server/web-server/routes/ask-ai.js +1 -1
  69. package/dist/server/web-server/routes/auth.js +1 -1
  70. package/dist/server/web-server/routes/catalog/helpers/upsert-pages-stats.js +1 -1
  71. package/dist/server/web-server/routes/feedback.js +1 -1
  72. package/dist/server/web-server/routes/mcp-routes/mcp-oauth.d.ts +5 -4
  73. package/dist/server/web-server/routes/mcp-routes/mcp-oauth.js +1 -1
  74. package/dist/server/web-server/routes/mcp-routes/mcp-routes.js +1 -1
  75. package/dist/server/web-server/routes/semantic-search.js +1 -1
  76. package/dist/server/workers/api-routes-worker-pool.js +1 -1
  77. package/dist/server/workers/mcp-tool-worker-pool.js +1 -1
  78. package/dist/server/workers/worker-pool.d.ts +1 -0
  79. package/dist/server/workers/worker-pool.js +1 -1
  80. package/package.json +7 -7
  81. package/dist/server/plugins/mcp/codemode/sandbox/sandbox-polyfills.d.ts +0 -3
  82. package/dist/server/plugins/mcp/codemode/sandbox/sandbox.d.ts +0 -3
  83. package/dist/server/plugins/mcp/codemode/sandbox/sandbox.js +0 -15
@@ -1 +1 @@
1
- import{RbacFeatures as p}from"../../../constants/common.js";import{ASK_AI_API_URL as a}from"../../constants/common.js";import{canAccessFeature as h}from"../../utils/rbac.js";import{handleUnauthorizedApiRequest as f}from"../utils.js";import{isAiSearchEnabled as A}from"../../plugins/search/utils.js";function R(c){return async e=>{if(!a)return e.newResponse(null,404);const t=c.getConfig();if(!A(t))return e.newResponse(null,403,{});const o=e.get("auth");if(!h(p.AI_SEARCH,o,t.access?.rbac,t.access?.requiresLogin))return f(e);const i={"Content-Type":"application/json"},n=[];o.claims?.authCookie&&n.push(`authorization=${o.claims.authCookie}`),o.idpAccessToken&&n.push(`accessToken=${o.idpAccessToken}`),n.length>0&&(i.Cookie=n.join("; "));const r=e.req.header("Authorization");r&&(i.Authorization=r);const u=await e.req.json(),s=await fetch(a,{method:"POST",body:JSON.stringify(u),headers:i});return s.ok?e.newResponse(s.body,200,{"Content-Type":"text/event-stream"}):e.newResponse(s.body,s.status,{"Content-Type":"application/json"})}}export{R as askAiHandler};
1
+ import{RbacFeatures as p}from"../../../constants/common.js";import{ASK_AI_API_URL as a}from"../../constants/common.js";import{fetchWithClient as h}from"../../utils/fetch-with-client.js";import{canAccessFeature as f}from"../../utils/rbac.js";import{handleUnauthorizedApiRequest as m}from"../utils.js";import{isAiSearchEnabled as A}from"../../plugins/search/utils.js";function g(c){return async e=>{if(!a)return e.newResponse(null,404);const t=c.getConfig();if(!A(t))return e.newResponse(null,403,{});const o=e.get("auth");if(!f(p.AI_SEARCH,o,t.access?.rbac,t.access?.requiresLogin))return m(e);const i={"Content-Type":"application/json"},n=[];o.claims?.authCookie&&n.push(`authorization=${o.claims.authCookie}`),o.idpAccessToken&&n.push(`accessToken=${o.idpAccessToken}`),n.length>0&&(i.Cookie=n.join("; "));const r=e.req.header("Authorization");r&&(i.Authorization=r);const u=await e.req.json(),s=await h(a,{method:"POST",body:JSON.stringify(u),headers:i});return s.ok?e.newResponse(s.body,200,{"Content-Type":"text/event-stream"}):e.newResponse(s.body,s.status,{"Content-Type":"application/json"})}}export{g as askAiHandler};
@@ -1 +1 @@
1
- import{setCookie as R,deleteCookie as Y}from"hono/cookie";import{AuthProviderType as Q,DEFAULT_TEAM_CLAIM_NAME as x}from"@redocly/config";import{withPathPrefix as _,getPathPrefix as y}from"@redocly/theme/core/utils";import{compareURIs as ee}from"../../../utils/url/compare-uris.js";import{ensureArray as q}from"../../../utils/array/ensure-array.js";import{ALTERNATIVE_AUD_CLAIM_NAME as F,JWT_SECRET_KEY as v,ORG_SLUG as re,ORG_ID as oe}from"../../constants/common.js";import{AUTH_SEGMENT as ne,ServerRoutes as P}from"../../../constants/api.js";import{AuthCookieNames as L,DEFAULT_COOKIE_EXPIRATION as H}from"../../../constants/common.js";import{sanitizeRedirectPathname as B}from"../../../utils/url/sanitize-redirect-pathname.js";import{telemetry as T}from"../../telemetry/index.js";import{envConfig as j}from"../../config/env-config.js";import{clearAuthCookies as K,clearPostLogoutCookie as $,hasPostLogoutCookie as te,setPostLogoutCookie as z}from"../../utils/cookie.js";import{getAuthProviderLoginParams as se,isOidcProviderConfig as E,isSaml2ProviderConfig as ie,oidcExchangeCodeForToken as ae,buildLoginUrl as de,decodeSamlResponse as ce,extractUserClaims as le,parseSamlResponse as ue,parseOidcState as pe,verifySAMLResponse as ge,getUsernameFromPayload as fe,buildOidcLogoutUrl as me,getOidcMetadata as G,getRedoclyTokenPayload as he,isRedoclySso as Ce,rewritePreviewAuthRedirectUri as Ae,parsePreviewBranch as J,buildOidcLoginUrl as Ie,createMcpSessionResource as D}from"../auth.js";import*as k from"../jwt/jwt.js";import{AlgorithmTypes as U}from"../jwt/types.js";import{handleErrorPageRender as we}from"../utils.js";import{encodeBase64URL as Le}from"../jwt/encode.js";import{resolveUiLocalesForIdpLogin as Re}from"./helpers/resolve-ui-locales-for-idp-login.js";async function He(s){if(j.isProductionEnv)return s.newResponse(null,404,{});const{password:e,...r}=await s.req.json(),a=await k.sign({...r,name:r.username||r.email||"Unknown"},v,U.HS256);return R(s,L.AUTHORIZATION,a,{path:y()||"/",httpOnly:!0,secure:!0,sameSite:"none"}),$(s),s.newResponse(null,200,{})}function Be(){return async s=>{const e=s.get("logger"),r=encodeURIComponent(s.req.query("message")||"");e.error(`Login error: ${r}`);const a=`${P.LOGIN}/?error=${encodeURIComponent(r)}`;return s.newResponse(null,301,{Location:a})}}function Z(s){if(!s||!s.includes(P.MCP_CALLBACK))return null;try{const e=s.split("/"),r=e[e.length-1];if(r){const a=Buffer.from(r,"base64url").toString("utf-8");return JSON.parse(a).mcpSessionId||null}}catch{}return null}function je(s){return async e=>{const r=e.get("logger"),a=s.getConfig().ssoDirect,o=pe(e.req.query("state")),m=o.idpId,t=o.source==="mcp"||o.redirectTo&&typeof o.redirectTo=="string"&&o.redirectTo.includes(P.MCP_CALLBACK),c=t?Z(typeof o.redirectTo=="string"?o.redirectTo:void 0):null,i=a?.[m];if(!E(i))return r.error("OIDC login error: missing OIDC provider config"),e.text("Forbidden",403);const d=await G(m,i);if(a&&!d.token_endpoint){const p="Invalid OIDC configuration: token_endpoint is required";return r.error(`OIDC login error: ${p}`),e.text(p,500)}try{const p=d.token_endpoint,u=e.req.query("code"),h=e.req.query("error");if(h)return t&&T.sendMcpAuthorizationFailedMessage([{...D(c),error:`OIDC error: ${h}`,error_details:e.req.query("error_description")||null}]),we(e,s,{slug:"/"},403,"403OIDC");if(!u){const A="Code is expected but not present";return r.error(`OIDC login error: ${A}`),t&&T.sendMcpAuthorizationFailedMessage([{...D(c),error:A,error_details:null}]),new Response(`Forbidden: ${A}`,{status:403})}const w=typeof o.redirectUri=="string"?o.redirectUri:new URL(_(P.OIDC_CALLBACK),e.req.url).toString(),C=e.get("cookies")?.code_verifier,l=await ae(p,u,w,i,{...i.tokenRequestCustomParams,...C?{code_verifier:C}:{}});if(l.error)return r.error(`Error from OIDC provider: "${l.error}"`),t&&T.sendMcpAuthorizationFailedMessage([{...D(c),error:`Token exchange error: ${l.error}`,error_details:l.error_description||null}]),e.text(`Forbidden: ${l.error_description||l.error}`,403);if(!l?.id_token){const A="No id_token, please, add openid to scopes";return r.error(`OIDC login error: ${A}`),t&&T.sendMcpAuthorizationFailedMessage([{...D(c),error:A,error_details:null}]),new Response(`Forbidden: ${A}`,{status:403})}const{payload:f,header:M}=k.decode(l.id_token),n=M.alg===U.RS256;if(i.audience?.length&&![...q(f.aud||[]),...q(f[F]||[])].some(O=>i.audience?.includes(O))){const O="No valid audience found in id_token";return r.error(`OIDC login error: ${O}`),t&&T.sendMcpAuthorizationFailedMessage([{...D(c),error:O,error_details:null}]),new Response(`Forbidden: ${O}`)}const g=n?l.id_token:await k.sign({...f,idpId:m},v,U.HS256);fe(f)||r.warn("To display your username, the required 'email' or 'full_profile' scope must be added to the identity provider configuration");const S=i?.tokenExpirationTime?Date.now()+i.tokenExpirationTime*1e3:f.exp*1e3||Date.now()+H*1e3;if(i.introspectEndpoint){const A=await fetch(i.introspectEndpoint,{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({access_token:l.access_token})});if(A.ok){const b=(await A.json()).ext?.federatedIdentity;b&&(R(e,L.FEDERATED_ACCESS_TOKEN,b.access_token||"",{path:y()||"/",httpOnly:!1,expires:new Date(S)}),R(e,L.FEDERATED_ID_TOKEN,b.id_token||"",{path:y()||"/",httpOnly:!1,expires:new Date(S)}))}else r.warn(`OIDC introspect error: ${A.statusText}`)}if(R(e,L.AUTHORIZATION,g,{path:y()||"/",httpOnly:!0,expires:new Date(S)}),g!==l.id_token&&R(e,L.IDP_ID_TOKEN,l.id_token||"",{path:y()||"/",httpOnly:!0,expires:new Date(S)}),R(e,L.IDP_ACCESS_TOKEN,l.access_token||"",{path:y()||"/",httpOnly:!0,expires:new Date(S)}),Y(e,"code_verifier",{path:y()||"/"}),$(e),t&&o.redirectTo&&typeof o.redirectTo=="string"&&o.redirectTo.includes(P.MCP_CALLBACK)){const O=`${e.req.url.split("?")[0].replace(P.OIDC_CALLBACK,"")}${o.redirectTo}`;return e.newResponse(null,302,{Location:O})}const V=typeof o.redirectTo=="string"?o.redirectTo:void 0;let X=B(new URL(V||"/",e.req.url).pathname);const W=e.newResponse(null,302,{Location:X});return r.updateContext({email:f.email,subject:f.sub}),r.info("OIDC login successful"),W}catch(p){const u=p instanceof Error?p.message:String(p),h=p instanceof Error?p.stack:String(p);if(r.error(`OIDC login error: ${u}`),t&&T.sendMcpAuthorizationFailedMessage([{...D(c),error:u,error_details:h}]),p.error==="access_denied")return r.info("Access denied"),e.text("Forbidden",403)}const I="Something went wrong";return r.error(`OIDC login error: ${I}`),t&&T.sendMcpAuthorizationFailedMessage([{...D(c),error:I,error_details:null}]),e.text(I,500)}}function Ke(s){return async e=>{const r=e.get("logger"),o=e.get("auth").claims?.idpId,t=s.getConfig().ssoDirect?.[o];if(e.req.method==="POST")return E(t)||(K(e),z(e)),r.info("Logout successful"),e.newResponse(null,200,{});let c;if(E(t)){const i=(await G(o,t)).end_session_endpoint;if(i){const d=new URL(e.req.url),I=e.req.header("x-forwarded-proto")||d.protocol.slice(0,-1)||"https",p=e.req.header("x-forwarded-host")||d.host,u=`${I}://${p}`,h=J(u),w=h?Le(JSON.stringify({branch:J(u)})):void 0,C=h?`${Ae(u)}/${ne}/logout`:`${u}${_(P.POST_LOGOUT)}`;c=me(i,C,e.get("cookies")?.[L.IDP_ID_TOKEN]||e.get("cookies")?.[L.AUTHORIZATION]||"",w)}}return r.info("Logout successful"),K(e),z(e),e.newResponse(null,302,{Location:c||_("/")})}}function ze(s){return async e=>{const r=s.getConfig().access?.logoutReturnUrl,a=r||_("/");return e.newResponse(null,302,{Location:a})}}function Ge(s){return async e=>{const r=e.get("logger"),a=e.req.param("code"),o=j.BH_API_URL,m=(t,c,i)=>t&&c?`${t} ${c.charAt(0)}`:i;try{if(!o)throw new Error("BH_API_URL is not set");const t=s.getConfig().ssoDirect;if(!t||!Object.keys(t).length)return r.warn("Invite no sso configured to handle"),e.redirect(_("/"));const c=await fetch(`${o}/user-invites/public/${a}`);if(!c.ok)return c.status===404?(r.warn(`Invite ${a} not found redirect to homepage`),e.redirect(_("/"))):(r.error("Invite error",await c.text()),e.redirect(_("/")));const i=await c.json(),d=new URL(_("/invite"),e.req.url);return d.searchParams.set("code",a),d.searchParams.set("org",i.organization.name),d.searchParams.set("invitedBy",m(i.invitedBy.firstName,i.invitedBy.lastName,i.invitedBy.name)),e.newResponse(null,302,{Location:d.toString()})}catch(t){return r.error("Error processing invite",{error:t,inviteCode:a}),e.text(t.message||"Failed to process invite",400)}}}function Je(s){return async e=>{const r=e.get("logger"),a=s.getConfig().ssoDirect,o=new URL(e.req.url),m=e.req.query("inviteCode"),t=e.req.header("x-forwarded-proto")||o.protocol.slice(0,-1)||"https",c=e.req.header("x-forwarded-host")||o.host,i=`${t}://${c}`;let d=o.searchParams.get("idpId");const I=o.searchParams.get("redirectTo"),p=Object.keys(a||{})[0];d=d||p;const u=o.searchParams.get("mcp_redirect_uri"),h=!!u;if(!a?.[d]){const g="Invalid idpId";if(r.error(`IdP login error: ${g}`),h){const N=Z(I||void 0);T.sendMcpAuthorizationFailedMessage([{...D(N),error:g,error_details:null}])}return e.text(`Forbidden: ${g}`,403)}const C=Re({localePrefixParam:o.searchParams.get("localePrefix"),l10n:s.getGlobalData()?.l10n}),l=d&&a?await se(d,a[d]):void 0,f={};for(const g of Object.keys(l?.extraParams||{}))f[g]=o.searchParams.get(g)||l?.extraParams?.[g]||void 0;te(e)&&(f.prompt="login",f.post_logout="1");let M,n={};if(h&&u&&l&&l.type===Q.OIDC){r.info(`Building MCP OAuth login URL with redirect_uri: ${u}`);const g=Ie("",{...l,extraParams:f},I,m,{redirectUriOverride:u,sourceOverride:"mcp",branchOverride:void 0,uiLocales:C});M=g.loginUrl,n=g.cookies||{}}else if(l){const g=de({...l,extraParams:f},i,I,m,C);M=g.loginUrl,n=g.cookies||{}}return Object.keys(n).forEach(g=>{R(e,g,n[g].value,n[g].options)}),r.info(`IdP login initiated for ID '${d}'`),e.newResponse(null,302,{Location:M||new URL(e.req.url).pathname})}}function Ze(s){return async e=>{const r=e.get("logger"),a=await e.req.formData(),o=a.get("SAMLResponse"),m=a.get("RelayState");if(typeof o!="string"||typeof m!="string"){const n="SAMLResponse is required";return r.error(`SAML2 login error: ${n}`),e.text(`Bad request: ${n}`,400)}const t=ce(o),{success:c,uid:i,nameFormat:d,attrs:I,issuerId:p,expiresAt:u}=ue(t),{idpId:h,redirectTo:w}=JSON.parse(m);if(!c){const n="SAML2 assertion is not successful";return r.error(`SAML2 login error: ${n}`),e.text(`Permission denied: ${n}`,401)}if(!u||Math.ceil(Date.now()/1e3)>=u){const n="SAML2 Token Expired";return r.error(`SAML2 login error: ${n}`),e.text(n,401)}const C=s.getConfig().ssoDirect?.[h];if(!C||!ie(C)){const n="Cannot find valid IdP";return r.error(`SAML2 login error: ${n}`),e.text(`Permission denied: ${n}`,401)}if(!(C.issuerId&&p&&ee(C.issuerId,p))){const n="IssuerID is misconfigured or untrusted assertions issuer received";return r.error(`SAML2 login error: ${n}`),e.text(`Permission denied: ${n}`,401)}if(!await ge(t,C.x509PublicCert)){const n="SAMLResponse signature invalid";return r.error(`SAML2 login error: ${n}`),e.text(n,401)}const f=le(i,d,I,C.teamsAttributeName??x);if(!f.sub){const n="The provider did not return a valid user identity.";return r.error(`SAML2 login error: ${n}`),e.text(n,400)}if(!f.email){const n="The provider did not return a valid user email.";return r.error(`SAML2 login error: ${n}`),e.text(n,400)}const M=await k.sign({...f,idpId:h},v,U.HS256);return R(e,L.AUTHORIZATION,M,{path:y()||"/",httpOnly:!0,expires:new Date(u*1e3)}),$(e),r.updateContext({email:f.email,subject:f.sub}),r.info("SAML2 login successful"),e.newResponse(null,302,{Location:w||"/"})}}function Ve(s){return async e=>{const r=e.get("logger"),a=new URL(e.req.query("redirectTo")||"/",e.req.url),o=_(B(a.pathname)),m=s.getConfig().ssoDirect,t=Object.entries(m||{}).find(([,w])=>E(w)&&Ce(w));if(!(m&&t))return e.newResponse(null,302,{Location:o});const i=e.req.query("token"),d=i&&await he(i);if(!d)return e.newResponse(null,302,{Location:o});if(!q(d[F]||[]).some(w=>w===re||w===oe))return e.newResponse(null,302,{Location:o});const u=await k.sign({...d,idpId:t?.at(0)},v,U.HS256),h=Date.now()+H*1e3;return R(e,L.AUTHORIZATION,u,{path:y()||"/",httpOnly:!0,expires:new Date(h),sameSite:"None",secure:!0}),$(e),r.info("Token login successful"),e.newResponse(null,302,{Location:o})}}export{He as authorizeHandler,Je as idpLoginHandler,Ge as inviteHandler,Ke as logoutHandler,je as oidcCallbackHandler,ze as postLogoutHandler,Be as redoclyLoginCallbackHandler,Ve as redoclyTokenLoginHandler,Ze as samlCallbackHandler};
1
+ import{setCookie as R,deleteCookie as Y}from"hono/cookie";import{AuthProviderType as Q,DEFAULT_TEAM_CLAIM_NAME as x}from"@redocly/config";import{withPathPrefix as _,getPathPrefix as y}from"@redocly/theme/core/utils";import{compareURIs as ee}from"../../../utils/url/compare-uris.js";import{ensureArray as q}from"../../../utils/array/ensure-array.js";import{ALTERNATIVE_AUD_CLAIM_NAME as F,JWT_SECRET_KEY as v,ORG_SLUG as re,ORG_ID as oe}from"../../constants/common.js";import{AUTH_SEGMENT as ne,ServerRoutes as P}from"../../../constants/api.js";import{AuthCookieNames as L,DEFAULT_COOKIE_EXPIRATION as H}from"../../../constants/common.js";import{sanitizeRedirectPathname as B}from"../../../utils/url/sanitize-redirect-pathname.js";import{telemetry as T}from"../../telemetry/index.js";import{envConfig as j}from"../../config/env-config.js";import{fetchWithClient as te}from"../../utils/fetch-with-client.js";import{clearAuthCookies as K,clearPostLogoutCookie as $,hasPostLogoutCookie as se,setPostLogoutCookie as z}from"../../utils/cookie.js";import{getAuthProviderLoginParams as ie,isOidcProviderConfig as E,isSaml2ProviderConfig as ae,oidcExchangeCodeForToken as de,buildLoginUrl as ce,decodeSamlResponse as le,extractUserClaims as ue,parseSamlResponse as pe,parseOidcState as ge,verifySAMLResponse as fe,getUsernameFromPayload as me,buildOidcLogoutUrl as he,getOidcMetadata as G,getRedoclyTokenPayload as Ce,isRedoclySso as Ae,rewritePreviewAuthRedirectUri as Ie,parsePreviewBranch as J,buildOidcLoginUrl as we,createMcpSessionResource as D}from"../auth.js";import*as k from"../jwt/jwt.js";import{AlgorithmTypes as U}from"../jwt/types.js";import{handleErrorPageRender as Le}from"../utils.js";import{encodeBase64URL as Re}from"../jwt/encode.js";import{resolveUiLocalesForIdpLogin as _e}from"./helpers/resolve-ui-locales-for-idp-login.js";async function je(s){if(j.isProductionEnv)return s.newResponse(null,404,{});const{password:e,...r}=await s.req.json(),a=await k.sign({...r,name:r.username||r.email||"Unknown"},v,U.HS256);return R(s,L.AUTHORIZATION,a,{path:y()||"/",httpOnly:!0,secure:!0,sameSite:"none"}),$(s),s.newResponse(null,200,{})}function Ke(){return async s=>{const e=s.get("logger"),r=encodeURIComponent(s.req.query("message")||"");e.error(`Login error: ${r}`);const a=`${P.LOGIN}/?error=${encodeURIComponent(r)}`;return s.newResponse(null,301,{Location:a})}}function Z(s){if(!s||!s.includes(P.MCP_CALLBACK))return null;try{const e=s.split("/"),r=e[e.length-1];if(r){const a=Buffer.from(r,"base64url").toString("utf-8");return JSON.parse(a).mcpSessionId||null}}catch{}return null}function ze(s){return async e=>{const r=e.get("logger"),a=s.getConfig().ssoDirect,o=ge(e.req.query("state")),m=o.idpId,t=o.source==="mcp"||o.redirectTo&&typeof o.redirectTo=="string"&&o.redirectTo.includes(P.MCP_CALLBACK),c=t?Z(typeof o.redirectTo=="string"?o.redirectTo:void 0):null,i=a?.[m];if(!E(i))return r.error("OIDC login error: missing OIDC provider config"),e.text("Forbidden",403);const d=await G(m,i);if(a&&!d.token_endpoint){const p="Invalid OIDC configuration: token_endpoint is required";return r.error(`OIDC login error: ${p}`),e.text(p,500)}try{const p=d.token_endpoint,u=e.req.query("code"),h=e.req.query("error");if(h)return t&&T.sendMcpAuthorizationFailedMessage([{...D(c),error:`OIDC error: ${h}`,error_details:e.req.query("error_description")||null}]),Le(e,s,{slug:"/"},403,"403OIDC");if(!u){const A="Code is expected but not present";return r.error(`OIDC login error: ${A}`),t&&T.sendMcpAuthorizationFailedMessage([{...D(c),error:A,error_details:null}]),new Response(`Forbidden: ${A}`,{status:403})}const w=typeof o.redirectUri=="string"?o.redirectUri:new URL(_(P.OIDC_CALLBACK),e.req.url).toString(),C=e.get("cookies")?.code_verifier,l=await de(p,u,w,i,{...i.tokenRequestCustomParams,...C?{code_verifier:C}:{}});if(l.error)return r.error(`Error from OIDC provider: "${l.error}"`),t&&T.sendMcpAuthorizationFailedMessage([{...D(c),error:`Token exchange error: ${l.error}`,error_details:l.error_description||null}]),e.text(`Forbidden: ${l.error_description||l.error}`,403);if(!l?.id_token){const A="No id_token, please, add openid to scopes";return r.error(`OIDC login error: ${A}`),t&&T.sendMcpAuthorizationFailedMessage([{...D(c),error:A,error_details:null}]),new Response(`Forbidden: ${A}`,{status:403})}const{payload:f,header:M}=k.decode(l.id_token),n=M.alg===U.RS256;if(i.audience?.length&&![...q(f.aud||[]),...q(f[F]||[])].some(O=>i.audience?.includes(O))){const O="No valid audience found in id_token";return r.error(`OIDC login error: ${O}`),t&&T.sendMcpAuthorizationFailedMessage([{...D(c),error:O,error_details:null}]),new Response(`Forbidden: ${O}`)}const g=n?l.id_token:await k.sign({...f,idpId:m},v,U.HS256);me(f)||r.warn("To display your username, the required 'email' or 'full_profile' scope must be added to the identity provider configuration");const S=i?.tokenExpirationTime?Date.now()+i.tokenExpirationTime*1e3:f.exp*1e3||Date.now()+H*1e3;if(i.introspectEndpoint){const A=await fetch(i.introspectEndpoint,{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({access_token:l.access_token})});if(A.ok){const b=(await A.json()).ext?.federatedIdentity;b&&(R(e,L.FEDERATED_ACCESS_TOKEN,b.access_token||"",{path:y()||"/",httpOnly:!1,expires:new Date(S)}),R(e,L.FEDERATED_ID_TOKEN,b.id_token||"",{path:y()||"/",httpOnly:!1,expires:new Date(S)}))}else r.warn(`OIDC introspect error: ${A.statusText}`)}if(R(e,L.AUTHORIZATION,g,{path:y()||"/",httpOnly:!0,expires:new Date(S)}),g!==l.id_token&&R(e,L.IDP_ID_TOKEN,l.id_token||"",{path:y()||"/",httpOnly:!0,expires:new Date(S)}),R(e,L.IDP_ACCESS_TOKEN,l.access_token||"",{path:y()||"/",httpOnly:!0,expires:new Date(S)}),Y(e,"code_verifier",{path:y()||"/"}),$(e),t&&o.redirectTo&&typeof o.redirectTo=="string"&&o.redirectTo.includes(P.MCP_CALLBACK)){const O=`${e.req.url.split("?")[0].replace(P.OIDC_CALLBACK,"")}${o.redirectTo}`;return e.newResponse(null,302,{Location:O})}const V=typeof o.redirectTo=="string"?o.redirectTo:void 0;let W=B(new URL(V||"/",e.req.url).pathname);const X=e.newResponse(null,302,{Location:W});return r.updateContext({email:f.email,subject:f.sub}),r.info("OIDC login successful"),X}catch(p){const u=p instanceof Error?p.message:String(p),h=p instanceof Error?p.stack:String(p);if(r.error(`OIDC login error: ${u}`),t&&T.sendMcpAuthorizationFailedMessage([{...D(c),error:u,error_details:h}]),p.error==="access_denied")return r.info("Access denied"),e.text("Forbidden",403)}const I="Something went wrong";return r.error(`OIDC login error: ${I}`),t&&T.sendMcpAuthorizationFailedMessage([{...D(c),error:I,error_details:null}]),e.text(I,500)}}function Ge(s){return async e=>{const r=e.get("logger"),o=e.get("auth").claims?.idpId,t=s.getConfig().ssoDirect?.[o];if(e.req.method==="POST")return E(t)||(K(e),z(e)),r.info("Logout successful"),e.newResponse(null,200,{});let c;if(E(t)){const i=(await G(o,t)).end_session_endpoint;if(i){const d=new URL(e.req.url),I=e.req.header("x-forwarded-proto")||d.protocol.slice(0,-1)||"https",p=e.req.header("x-forwarded-host")||d.host,u=`${I}://${p}`,h=J(u),w=h?Re(JSON.stringify({branch:J(u)})):void 0,C=h?`${Ie(u)}/${ne}/logout`:`${u}${_(P.POST_LOGOUT)}`;c=he(i,C,e.get("cookies")?.[L.IDP_ID_TOKEN]||e.get("cookies")?.[L.AUTHORIZATION]||"",w)}}return r.info("Logout successful"),K(e),z(e),e.newResponse(null,302,{Location:c||_("/")})}}function Je(s){return async e=>{const r=s.getConfig().access?.logoutReturnUrl,a=r||_("/");return e.newResponse(null,302,{Location:a})}}function Ze(s){return async e=>{const r=e.get("logger"),a=e.req.param("code"),o=j.BH_API_URL,m=(t,c,i)=>t&&c?`${t} ${c.charAt(0)}`:i;try{if(!o)throw new Error("BH_API_URL is not set");const t=s.getConfig().ssoDirect;if(!t||!Object.keys(t).length)return r.warn("Invite no sso configured to handle"),e.redirect(_("/"));const c=await te(`${o}/user-invites/public/${a}`);if(!c.ok)return c.status===404?(r.warn(`Invite ${a} not found redirect to homepage`),e.redirect(_("/"))):(r.error("Invite error",await c.text()),e.redirect(_("/")));const i=await c.json(),d=new URL(_("/invite"),e.req.url);return d.searchParams.set("code",a),d.searchParams.set("org",i.organization.name),d.searchParams.set("invitedBy",m(i.invitedBy.firstName,i.invitedBy.lastName,i.invitedBy.name)),e.newResponse(null,302,{Location:d.toString()})}catch(t){return r.error("Error processing invite",{error:t,inviteCode:a}),e.text(t.message||"Failed to process invite",400)}}}function Ve(s){return async e=>{const r=e.get("logger"),a=s.getConfig().ssoDirect,o=new URL(e.req.url),m=e.req.query("inviteCode"),t=e.req.header("x-forwarded-proto")||o.protocol.slice(0,-1)||"https",c=e.req.header("x-forwarded-host")||o.host,i=`${t}://${c}`;let d=o.searchParams.get("idpId");const I=o.searchParams.get("redirectTo"),p=Object.keys(a||{})[0];d=d||p;const u=o.searchParams.get("mcp_redirect_uri"),h=!!u;if(!a?.[d]){const g="Invalid idpId";if(r.error(`IdP login error: ${g}`),h){const N=Z(I||void 0);T.sendMcpAuthorizationFailedMessage([{...D(N),error:g,error_details:null}])}return e.text(`Forbidden: ${g}`,403)}const C=_e({localePrefixParam:o.searchParams.get("localePrefix"),l10n:s.getGlobalData()?.l10n}),l=d&&a?await ie(d,a[d]):void 0,f={};for(const g of Object.keys(l?.extraParams||{}))f[g]=o.searchParams.get(g)||l?.extraParams?.[g]||void 0;se(e)&&(f.prompt="login",f.post_logout="1");let M,n={};if(h&&u&&l&&l.type===Q.OIDC){r.info(`Building MCP OAuth login URL with redirect_uri: ${u}`);const g=we("",{...l,extraParams:f},I,m,{redirectUriOverride:u,sourceOverride:"mcp",branchOverride:void 0,uiLocales:C});M=g.loginUrl,n=g.cookies||{}}else if(l){const g=ce({...l,extraParams:f},i,I,m,C);M=g.loginUrl,n=g.cookies||{}}return Object.keys(n).forEach(g=>{R(e,g,n[g].value,n[g].options)}),r.info(`IdP login initiated for ID '${d}'`),e.newResponse(null,302,{Location:M||new URL(e.req.url).pathname})}}function We(s){return async e=>{const r=e.get("logger"),a=await e.req.formData(),o=a.get("SAMLResponse"),m=a.get("RelayState");if(typeof o!="string"||typeof m!="string"){const n="SAMLResponse is required";return r.error(`SAML2 login error: ${n}`),e.text(`Bad request: ${n}`,400)}const t=le(o),{success:c,uid:i,nameFormat:d,attrs:I,issuerId:p,expiresAt:u}=pe(t),{idpId:h,redirectTo:w}=JSON.parse(m);if(!c){const n="SAML2 assertion is not successful";return r.error(`SAML2 login error: ${n}`),e.text(`Permission denied: ${n}`,401)}if(!u||Math.ceil(Date.now()/1e3)>=u){const n="SAML2 Token Expired";return r.error(`SAML2 login error: ${n}`),e.text(n,401)}const C=s.getConfig().ssoDirect?.[h];if(!C||!ae(C)){const n="Cannot find valid IdP";return r.error(`SAML2 login error: ${n}`),e.text(`Permission denied: ${n}`,401)}if(!(C.issuerId&&p&&ee(C.issuerId,p))){const n="IssuerID is misconfigured or untrusted assertions issuer received";return r.error(`SAML2 login error: ${n}`),e.text(`Permission denied: ${n}`,401)}if(!await fe(t,C.x509PublicCert)){const n="SAMLResponse signature invalid";return r.error(`SAML2 login error: ${n}`),e.text(n,401)}const f=ue(i,d,I,C.teamsAttributeName??x);if(!f.sub){const n="The provider did not return a valid user identity.";return r.error(`SAML2 login error: ${n}`),e.text(n,400)}if(!f.email){const n="The provider did not return a valid user email.";return r.error(`SAML2 login error: ${n}`),e.text(n,400)}const M=await k.sign({...f,idpId:h},v,U.HS256);return R(e,L.AUTHORIZATION,M,{path:y()||"/",httpOnly:!0,expires:new Date(u*1e3)}),$(e),r.updateContext({email:f.email,subject:f.sub}),r.info("SAML2 login successful"),e.newResponse(null,302,{Location:w||"/"})}}function Xe(s){return async e=>{const r=e.get("logger"),a=new URL(e.req.query("redirectTo")||"/",e.req.url),o=_(B(a.pathname)),m=s.getConfig().ssoDirect,t=Object.entries(m||{}).find(([,w])=>E(w)&&Ae(w));if(!(m&&t))return e.newResponse(null,302,{Location:o});const i=e.req.query("token"),d=i&&await Ce(i);if(!d)return e.newResponse(null,302,{Location:o});if(!q(d[F]||[]).some(w=>w===re||w===oe))return e.newResponse(null,302,{Location:o});const u=await k.sign({...d,idpId:t?.at(0)},v,U.HS256),h=Date.now()+H*1e3;return R(e,L.AUTHORIZATION,u,{path:y()||"/",httpOnly:!0,expires:new Date(h),sameSite:"None",secure:!0}),$(e),r.info("Token login successful"),e.newResponse(null,302,{Location:o})}}export{je as authorizeHandler,Ve as idpLoginHandler,Ze as inviteHandler,Ge as logoutHandler,ze as oidcCallbackHandler,Je as postLogoutHandler,Ke as redoclyLoginCallbackHandler,Xe as redoclyTokenLoginHandler,We as samlCallbackHandler};
@@ -1 +1 @@
1
- import{envConfig as t}from"../../../../config/env-config.js";async function u({ctx:r,catalogEntitiesService:s,addedEntities:n,removedEntities:i}){if(!t.isProductionEnv){r.get("logger").info("Skipping pages stats upsert in non-production environment");return}if(!t.BH_API_URL||!t.ORGANIZATION_ID||!t.PROJECT_ID)throw new Error("Pages stats service not configured");const o=r.req.header("apiKey");if(!o)throw new Error("API key is required to upsert pages stats");const{total:a}=await s.getEntitiesCount("remote",n,i?.length),p={remoteEntityPages:a,isReported:!1},g=new URL(`/api/orgs/${t.ORGANIZATION_ID}/projects/${t.PROJECT_ID}/project-pages-stats`,t.BH_API_URL).toString();try{const e=await fetch(g,{method:"PUT",headers:{"Content-Type":"application/json",Authorization:`Bearer ${o}`},body:JSON.stringify(p)});if(!e.ok)throw new Error(e.statusText)}catch(e){throw new Error(`Failed to upsert project pages stats: ${e?.message}`)}}export{u as upsertPagesStats};
1
+ import{envConfig as t}from"../../../../config/env-config.js";import{fetchWithClient as c}from"../../../../utils/fetch-with-client.js";async function h({ctx:r,catalogEntitiesService:s,addedEntities:n,removedEntities:i}){if(!t.isProductionEnv){r.get("logger").info("Skipping pages stats upsert in non-production environment");return}if(!t.BH_API_URL||!t.ORGANIZATION_ID||!t.PROJECT_ID)throw new Error("Pages stats service not configured");const o=r.req.header("apiKey");if(!o)throw new Error("API key is required to upsert pages stats");const{total:a}=await s.getEntitiesCount("remote",n,i?.length),p={remoteEntityPages:a,isReported:!1},g=new URL(`/api/orgs/${t.ORGANIZATION_ID}/projects/${t.PROJECT_ID}/project-pages-stats`,t.BH_API_URL).toString();try{const e=await c(g,{method:"PUT",headers:{"Content-Type":"application/json",Authorization:`Bearer ${o}`},body:JSON.stringify(p)});if(!e.ok)throw new Error(e.statusText)}catch(e){throw new Error(`Failed to upsert project pages stats: ${e?.message}`)}}export{h as upsertPagesStats};
@@ -1 +1 @@
1
- import{FEEDBACK_API_URL as E}from"../../constants/common.js";import{MAX_AGENT_LENGTH as T,MAX_CONTEXT_LENGTH as A,MAX_EMAIL_LENGTH as k,MAX_LANG_LENGTH as y,MAX_PATH_LENGTH as N,MAX_REASONS_COUNT as F,MAX_TARGET_FEATURE_LENGTH as j}from"../../constants/feedback.js";import{mapObject as L}from"../../../utils/object/map-object.js";import{getFeedbackClientIp as C}from"../utils/feedback-ip.js";import{canAccessResource as R}from"../../utils/rbac.js";function r(n,e){if(n!=null)return String(n).replace(/[\x00-\x08\x0B\x0C\x0E-\x1F\x7F]/g,"").trim().slice(0,e)||void 0}function S(n){if(!n)return n;const e=["userAgent","firstName","lastName","auth_time","platform","id","email","ipAddress"];return L(n,(t,a)=>e.includes(a)?t:"*****")}function G({component:n,path:e,location:t,lang:a,agent:p,targetFeature:m,score:s,max:o,reasons:i,comment:f,metadata:l,email:h}){const u=Array.isArray(i)?i.map(c=>r(String(c),A)).filter(c=>!!c).slice(0,F):void 0;return{feedbackComponent:n.toUpperCase(),path:r(e,N),location:r(t,A),lang:r(a,y),score:typeof s=="number"?s:void 0,maxScore:typeof o=="number"?o:void 0,reasons:u?.length?u:void 0,comment:r(f,A),email:r(h,k),agent:r(p,T),targetFeature:r(m,j),metadata:S(l)}}async function M(n,e){return(await fetch(E,{method:"POST",body:JSON.stringify(G(n)),headers:e})).json()}function P(n){return async e=>{const t=await e.req.json(),a=e.req.header("user-agent"),p=C(e.req.raw),m=e.req.header("Sec-Ch-Ua-Platform"),s={...t.metadata,userAgent:a,ipAddress:p,platform:m?m.replace(/"/g,""):"unknown"},o=[];(!t.path||t.path==="")&&o.push("`path` is required");const i=["sentiment","rating","comment","problem","mood","scale"];if(i.includes(t.component)||o.push(`\`component\` field should be one of ${i.join(", ")}.`),o.length)return e.json({errors:o},400);const{claims:f,isAuthenticated:l,teams:h}=e.get("auth"),u={isAuthenticated:l,email:f?.email,teams:h};if(Object.keys(n.config.access?.rbac||{}).length>0){const d=t.path,_=new URL(d).pathname,b=n.getRouteBySlug(_);if(!b)return e.json({errors:["Resource not found"]},404);if(!R(b,u,n.config.access?.rbac,n.config.access?.requiresLogin))return e.json({errors:["Forbidden: no permission to send feedback for resource"]},403)}const c={"Content-Type":"application/json"},g=f?.email||t?.email||s?.email;try{const d=await M({...t,email:g,metadata:{email:g,...s}},c);return e.json({message:"Thanks for your feedback",...d},200,{})}catch(d){return e.json({errors:["Failed to send feedback",d.message]},500)}}}export{P as feedbackHandler,G as normalizeFeedbackData,S as normalizeFeedbackMetadata};
1
+ import{FEEDBACK_API_URL as E}from"../../constants/common.js";import{fetchWithClient as T}from"../../utils/fetch-with-client.js";import{MAX_AGENT_LENGTH as k,MAX_CONTEXT_LENGTH as A,MAX_EMAIL_LENGTH as y,MAX_LANG_LENGTH as N,MAX_PATH_LENGTH as F,MAX_REASONS_COUNT as j,MAX_TARGET_FEATURE_LENGTH as C}from"../../constants/feedback.js";import{mapObject as L}from"../../../utils/object/map-object.js";import{getFeedbackClientIp as R}from"../utils/feedback-ip.js";import{canAccessResource as S}from"../../utils/rbac.js";function r(n,e){if(n!=null)return String(n).replace(/[\x00-\x08\x0B\x0C\x0E-\x1F\x7F]/g,"").trim().slice(0,e)||void 0}function G(n){if(!n)return n;const e=["userAgent","firstName","lastName","auth_time","platform","id","email","ipAddress"];return L(n,(t,a)=>e.includes(a)?t:"*****")}function M({component:n,path:e,location:t,lang:a,agent:p,targetFeature:m,score:s,max:o,reasons:i,comment:f,metadata:l,email:h}){const u=Array.isArray(i)?i.map(c=>r(String(c),A)).filter(c=>!!c).slice(0,j):void 0;return{feedbackComponent:n.toUpperCase(),path:r(e,F),location:r(t,A),lang:r(a,N),score:typeof s=="number"?s:void 0,maxScore:typeof o=="number"?o:void 0,reasons:u?.length?u:void 0,comment:r(f,A),email:r(h,y),agent:r(p,k),targetFeature:r(m,C),metadata:G(l)}}async function w(n,e){return(await T(E,{method:"POST",body:JSON.stringify(M(n)),headers:e})).json()}function B(n){return async e=>{const t=await e.req.json(),a=e.req.header("user-agent"),p=R(e.req.raw),m=e.req.header("Sec-Ch-Ua-Platform"),s={...t.metadata,userAgent:a,ipAddress:p,platform:m?m.replace(/"/g,""):"unknown"},o=[];(!t.path||t.path==="")&&o.push("`path` is required");const i=["sentiment","rating","comment","problem","mood","scale"];if(i.includes(t.component)||o.push(`\`component\` field should be one of ${i.join(", ")}.`),o.length)return e.json({errors:o},400);const{claims:f,isAuthenticated:l,teams:h}=e.get("auth"),u={isAuthenticated:l,email:f?.email,teams:h};if(Object.keys(n.config.access?.rbac||{}).length>0){const d=t.path,_=new URL(d).pathname,b=n.getRouteBySlug(_);if(!b)return e.json({errors:["Resource not found"]},404);if(!S(b,u,n.config.access?.rbac,n.config.access?.requiresLogin))return e.json({errors:["Forbidden: no permission to send feedback for resource"]},403)}const c={"Content-Type":"application/json"},g=f?.email||t?.email||s?.email;try{const d=await w({...t,email:g,metadata:{email:g,...s}},c);return e.json({message:"Thanks for your feedback",...d},200,{})}catch(d){return e.json({errors:["Failed to send feedback",d.message]},500)}}}export{B as feedbackHandler,M as normalizeFeedbackData,G as normalizeFeedbackMetadata};
@@ -1,4 +1,5 @@
1
1
  import type { Handler } from 'hono';
2
+ import type { Store } from '../../../store.js';
2
3
  export type McpContextPayload = {
3
4
  isMcpFlow: boolean;
4
5
  originalRedirectUri: string | null;
@@ -12,14 +13,14 @@ export type McpContextPayload = {
12
13
  export declare function verifyPkce(codeChallenge: string, codeChallengeMethod: string | undefined, codeVerifier: string): boolean;
13
14
  export declare function createMcpContextToken(context: McpContextPayload): Promise<string>;
14
15
  export declare function verifyAndParseMcpContextToken(token: string): Promise<McpContextPayload>;
15
- export declare function mcpOAuthProtectedResourceHandler(): Handler;
16
- export declare function mcpOAuthAuthorizationServerHandler(): Handler;
16
+ export declare function mcpOAuthProtectedResourceHandler(store: Store): Handler;
17
+ export declare function mcpOAuthAuthorizationServerHandler(store: Store): Handler;
17
18
  /**
18
19
  * This code is not used in the MCP server, but it is required to satisfy the OAuth 2.0 Dynamic Client Registration Protocol.
19
20
  * Also it is simulating the client registration process.
20
21
  */
21
- export declare function mcpDynamicClientRegistrationHandler(): Handler;
22
- export declare function mcpAuthorizationHandler(): Handler;
22
+ export declare function mcpDynamicClientRegistrationHandler(store: Store): Handler;
23
+ export declare function mcpAuthorizationHandler(store: Store): Handler;
23
24
  export declare function mcpTokenPortalHandler(): Handler;
24
25
  export declare function mcpCallbackHandler(): Handler;
25
26
  //# sourceMappingURL=mcp-oauth.d.ts.map
@@ -1 +1 @@
1
- import{getCookie as C}from"hono/cookie";import{createHash as U,timingSafeEqual as R}from"node:crypto";import{ulid as v}from"ulid";import{AUTH_URL as f,JWT_SECRET_KEY as M}from"../../../constants/common.js";import{ServerRoutes as _}from"../../../../constants/api.js";import{AuthCookieNames as T}from"../../../../constants/common.js";import{withPathPrefix as u}from"@redocly/theme/core/utils";import{telemetry as m}from"../../../telemetry/index.js";import{envConfig as A}from"../../../config/env-config.js";import{createMcpAuthorizationCode as O,verifyMcpAuthorizationCode as D,createMcpSessionResource as h}from"../../auth.js";import*as g from"../../jwt/jwt.js";import{AlgorithmTypes as S}from"../../jwt/types.js";import{getRequestOrigin as y}from"../../utils/get-request-origin.js";import{fetchClientMetadataDocument as L,isClientMetadataDocumentUrl as k,isRedirectUriRegistered as H}from"./mcp-client-metadata.js";const o=(e,r,n=200,a)=>e.json(r,n,{"Content-Type":"application/json",...a??{}}),I=new Set(["S256"]),q="https://claude.ai/oauth/claude-code-client-metadata";function z(e,r,n){if(!I.has(r||""))return!1;const a=U("sha256").update(n).digest("base64url"),t=Buffer.from(a),s=Buffer.from(e);return t.length!==s.length?!1:R(t,s)}async function $(e){const r=Math.floor(Date.now()/1e3);return g.sign({type:"mcp_context",...e,iat:r,exp:r+600},M,S.HS256)}async function N(e){await g.verify(e,M,S.HS256);const{payload:r}=g.decode(e);if(r.type!=="mcp_context")throw new Error("Invalid context token type");return r}function x(){return async e=>{if(e.req.method!=="GET")return o(e,{error:"Method not allowed"},405,{Allow:"GET"});const r=y(e);return o(e,{resource:`${r}${u("/mcp")}`,authorization_servers:[r,f].filter(Boolean),bearer_methods_supported:["header"],resource_documentation:`${r}${_.MCP_OAUTH_AUTHORIZATION_SERVER}`,scopes_supported:["openid","profile","email","offline_access"],bearer_token_types_supported:["Bearer"]})}}function ee(){return async e=>{const r=y(e);return o(e,{issuer:f||"",authorization_endpoint:`${r}${u(_.MCP_AUTHORIZATION)}`,token_endpoint:`${r}${u(_.MCP_TOKEN_PORTAL)}`,jwks_uri:`${f||""}/.well-known/jwks.json`,scopes_supported:["openid","profile","email","offline_access"],registration_endpoint:`${r}${u(_.MCP_DYNAMIC_CLIENT_REGISTRATION)}`,response_types_supported:["code"],grant_types_supported:["authorization_code","refresh_token","client_credentials"],subject_types_supported:["public"],id_token_signing_alg_values_supported:["RS256"],code_challenge_methods_supported:["S256"],token_endpoint_auth_methods_supported:["none"],client_id_metadata_document_supported:!0})}}function re(){return async e=>{if(e.req.method!=="POST")return o(e,{error:"Method not allowed"},405);try{return o(e,{client_id:A.OAUTH_CLIENT_ID||"",client_name:"MCP Client",redirect_uris:[],grant_types:["authorization_code","refresh_token"],response_types:["code"],scope:"openid offline email",subject_type:"public",token_endpoint_auth_method:"none",created_at:new Date().toISOString(),updated_at:new Date().toISOString()},201)}catch(r){return o(e,{error:"invalid_request",error_description:r?.message||"Unable to register client"},500)}}}function te(){return async e=>{const r=new URL(e.req.url),{searchParams:n}=r,a=n.get("redirect_uri"),t=n.get("client_id"),s=n.get("code_challenge"),i=n.get("code_challenge_method"),d=v();if(s&&!I.has(i||""))return o(e,{error:"invalid_request",error_description:`Unsupported code_challenge_method. Supported: ${[...I].join(", ")}`},400);if(k(t)&&!s)return o(e,{error:"invalid_request",error_description:"code_challenge is required for client_id_metadata_document clients"},400);if(k(t)&&a)try{const c=await L(t),l=t===q;if(!H(a,c.redirect_uris,{ignorePort:l}))return o(e,{error:"invalid_request",error_description:"redirect_uri is not registered for this client"},400)}catch(c){const l=c instanceof Error?c.message:String(c);return o(e,{error:"invalid_client",error_description:`Unable to resolve client metadata document: ${l}`},400)}m.sendMcpAuthorizationStartedMessage([{...h(d),redirect_uri:a||null}]);const p=y(e),w={isMcpFlow:!0,originalRedirectUri:a,mcpClientId:t,mcpState:n.get("state"),mcpSessionId:d,mcpCodeChallenge:s,mcpCodeChallengeMethod:s?i:null,timestamp:Date.now()};try{const c=await $(w),l=new URL(u(_.IDP_LOGIN),p);return l.searchParams.set("redirectTo",`${_.MCP_CALLBACK}/${c}`),l.searchParams.set("idpId","oidc"),e.redirect(l.toString())}catch(c){const l=c instanceof Error?c.message:String(c),P=c instanceof Error?c.stack:String(c);m.sendMcpAuthorizationFailedMessage([{...h(d),error:l,error_details:P}]);const E=new URL(u(`${f}/oauth2/auth`));return E.search=n.toString(),e.redirect(E.toString())}}}function oe(){return async e=>{if(e.req.method!=="POST")return o(e,{error:"Method not allowed"},405);try{const r=await e.req.formData(),n=r.get("grant_type"),a=r.get("code"),t=r.get("redirect_uri")||void 0,s=r.get("code_verifier")||void 0;if(n!=="authorization_code"||!a)return o(e,{error:"invalid_request",error_description:"Invalid grant type or missing authorization code"},400);try{const i=await D(a);if(t&&t!==i.redirect_uri)return o(e,{error:"invalid_grant",error_description:"redirect_uri mismatch"},400);if(i.code_challenge){if(!s)return o(e,{error:"invalid_grant",error_description:"code_verifier required"},400);if(!z(i.code_challenge,i.code_challenge_method,s))return o(e,{error:"invalid_grant",error_description:"code_verifier mismatch"},400)}if(A.OAUTH_CLIENT_ID&&i.client_id&&!k(i.client_id)&&i.client_id!==A.OAUTH_CLIENT_ID)return o(e,{error:"invalid_client",error_description:"Client mismatch"},400);const d=i.id_token;if(typeof d!="string"||d.length===0)return o(e,{error:"invalid_grant",error_description:"Missing id_token in authorization code"},400);let p=d;if(i.idp_access_token){const{payload:c,header:{kid:l}}=g.decode(d);p=await g.sign({...c,idp_access_token:i.idp_access_token},M,S.HS256,l)}return o(e,{access_token:p,token_type:"Bearer",expires_in:3600,scope:"openid profile email",id_token:d},200,{"Cache-Control":"no-store",Pragma:"no-cache"})}catch{return o(e,{error:"invalid_grant",error_description:"Invalid authorization code"},400)}}catch(r){const n=r instanceof Error?r.message:String(r);return o(e,{error:"server_error",error_description:"Failed to process token request",error_details:n},500)}}}function ne(){return async e=>{const r=new URL(e.req.url);let n=r.searchParams.get("context");if(!n&&r.pathname.startsWith(u(`${_.MCP_CALLBACK}/`))){const t=r.pathname.split("/");n=t[t.length-1]}if(!n)return m.sendMcpAuthorizationFailedMessage([{...h(null),error:"Missing context parameter",error_details:null}]),e.text("Missing context parameter",400);let a=null;try{const t=await N(n);if(a=t.mcpSessionId||null,!t.isMcpFlow||!t.originalRedirectUri)throw new Error("Invalid MCP context");const s=C(e,T.IDP_ID_TOKEN)||C(e,T.AUTHORIZATION),i=C(e,T.IDP_ACCESS_TOKEN);if(!s)return m.sendMcpAuthorizationFailedMessage([{...h(a),error:"Authentication required",error_details:null}]),e.text("Authentication required",401);const d=await O({idToken:s,idpAccessToken:i||void 0,clientId:t.mcpClientId||"",redirectUri:t.originalRedirectUri,codeChallenge:t.mcpCodeChallenge||void 0,codeChallengeMethod:t.mcpCodeChallengeMethod||void 0,ttlSec:600}),p=new URL(t.originalRedirectUri);return p.searchParams.set("code",d),t.mcpState&&p.searchParams.set("state",t.mcpState),m.sendMcpAuthorizationCompletedMessage([{...h(a),redirect_uri:t.originalRedirectUri||null}]),e.redirect(p.toString())}catch(t){const s=t instanceof Error?t.message:String(t),i=t instanceof Error?t.stack:String(t);return m.sendMcpAuthorizationFailedMessage([{...h(a),error:s,error_details:i}]),e.text(`Invalid MCP callback: ${s}`,400)}}}export{$ as createMcpContextToken,te as mcpAuthorizationHandler,ne as mcpCallbackHandler,re as mcpDynamicClientRegistrationHandler,ee as mcpOAuthAuthorizationServerHandler,x as mcpOAuthProtectedResourceHandler,oe as mcpTokenPortalHandler,N as verifyAndParseMcpContextToken,z as verifyPkce};
1
+ import{getCookie as S}from"hono/cookie";import{createHash as D,timingSafeEqual as L}from"node:crypto";import{ulid as H}from"ulid";import{isMcpContentProtected as C}from"../../../plugins/mcp/auth/auth-handlers.js";import{isRedoclySso as q,isOidcProviderConfig as z}from"../../auth.js";import{AUTH_URL as M,JWT_SECRET_KEY as A}from"../../../constants/common.js";import{ServerRoutes as u}from"../../../../constants/api.js";import{AuthCookieNames as k}from"../../../../constants/common.js";import{withPathPrefix as _}from"@redocly/theme/core/utils";import{telemetry as m}from"../../../telemetry/index.js";import{envConfig as I}from"../../../config/env-config.js";import{createMcpAuthorizationCode as $,verifyMcpAuthorizationCode as N,createMcpSessionResource as f}from"../../auth.js";import*as h from"../../jwt/jwt.js";import{AlgorithmTypes as w}from"../../jwt/types.js";import{getRequestOrigin as E}from"../../utils/get-request-origin.js";import{fetchClientMetadataDocument as b,isClientMetadataDocumentUrl as P,isRedirectUriRegistered as j}from"./mcp-client-metadata.js";const o=(r,e,n=200,a)=>r.json(e,n,{"Content-Type":"application/json",...a??{}}),U=new Set(["S256"]),B="https://claude.ai/oauth/claude-code-client-metadata";function F(r,e,n){if(!U.has(e||""))return!1;const a=D("sha256").update(n).digest("base64url"),t=Buffer.from(a),s=Buffer.from(r);return t.length!==s.length?!1:L(t,s)}async function K(r){const e=Math.floor(Date.now()/1e3);return h.sign({type:"mcp_context",...r,iat:e,exp:e+600},A,w.HS256)}async function G(r){await h.verify(r,A,w.HS256);const{payload:e}=h.decode(r);if(e.type!=="mcp_context")throw new Error("Invalid context token type");return e}function se(r){return async e=>{if(!C(r))return o(e,{error:"not_found"},404);if(e.req.method!=="GET")return o(e,{error:"Method not allowed"},405,{Allow:"GET"});const n=E(e);return o(e,{resource:`${n}${_("/mcp")}`,authorization_servers:[n,M].filter(Boolean),bearer_methods_supported:["header"],resource_documentation:`${n}${u.MCP_OAUTH_AUTHORIZATION_SERVER}`,scopes_supported:["openid","profile","email","offline_access"],bearer_token_types_supported:["Bearer"]})}}function ce(r){return async e=>{if(!C(r))return o(e,{error:"not_found"},404);const n=E(e);return o(e,{issuer:M||"",authorization_endpoint:`${n}${_(u.MCP_AUTHORIZATION)}`,token_endpoint:`${n}${_(u.MCP_TOKEN_PORTAL)}`,jwks_uri:`${M||""}/.well-known/jwks.json`,scopes_supported:["openid","profile","email","offline_access"],registration_endpoint:`${n}${_(u.MCP_DYNAMIC_CLIENT_REGISTRATION)}`,response_types_supported:["code"],grant_types_supported:["authorization_code","refresh_token","client_credentials"],subject_types_supported:["public"],id_token_signing_alg_values_supported:["RS256"],code_challenge_methods_supported:["S256"],token_endpoint_auth_methods_supported:["none"],client_id_metadata_document_supported:!0})}}function de(r){return async e=>{if(!C(r))return o(e,{error:"not_found"},404);if(e.req.method!=="POST")return o(e,{error:"Method not allowed"},405);try{return o(e,{client_id:I.OAUTH_CLIENT_ID||"",client_name:"MCP Client",redirect_uris:[],grant_types:["authorization_code","refresh_token"],response_types:["code"],scope:"openid offline email",subject_type:"public",token_endpoint_auth_method:"none",created_at:new Date().toISOString(),updated_at:new Date().toISOString()},201)}catch(n){return o(e,{error:"invalid_request",error_description:n?.message||"Unable to register client"},500)}}}function le(r){return async e=>{if(!C(r))return o(e,{error:"not_found"},404);const n=new URL(e.req.url),{searchParams:a}=n,t=a.get("redirect_uri"),s=a.get("client_id"),i=a.get("code_challenge"),d=a.get("code_challenge_method"),l=H();if(i&&!U.has(d||""))return o(e,{error:"invalid_request",error_description:`Unsupported code_challenge_method. Supported: ${[...U].join(", ")}`},400);if(P(s)&&!i)return o(e,{error:"invalid_request",error_description:"code_challenge is required for client_id_metadata_document clients"},400);if(P(s)&&t)try{const c=await b(s),p=s===B;if(!j(t,c.redirect_uris,{ignorePort:p}))return o(e,{error:"invalid_request",error_description:"redirect_uri is not registered for this client"},400)}catch(c){const p=c instanceof Error?c.message:String(c);return o(e,{error:"invalid_client",error_description:`Unable to resolve client metadata document: ${p}`},400)}m.sendMcpAuthorizationStartedMessage([{...f(l),redirect_uri:t||null}]);const v=E(e),T={isMcpFlow:!0,originalRedirectUri:t,mcpClientId:s,mcpState:a.get("state"),mcpSessionId:l,mcpCodeChallenge:i,mcpCodeChallengeMethod:i?d:null,timestamp:Date.now()};try{const c=await K(T),p=new URL(_(u.IDP_LOGIN),v);p.searchParams.set("redirectTo",`${u.MCP_CALLBACK}/${c}`);const g=r.getConfig().ssoDirect||{},R=Object.entries(g).find(([,O])=>z(O)&&q(O))?.[0]??Object.keys(g)[0];return R?(p.searchParams.set("idpId",R),e.redirect(p.toString())):o(e,{error:"not_found"},404)}catch(c){const p=c instanceof Error?c.message:String(c),g=c instanceof Error?c.stack:String(c);m.sendMcpAuthorizationFailedMessage([{...f(l),error:p,error_details:g}]);const y=new URL(_(`${M}/oauth2/auth`));return y.search=a.toString(),e.redirect(y.toString())}}}function pe(){return async r=>{if(r.req.method!=="POST")return o(r,{error:"Method not allowed"},405);try{const e=await r.req.formData(),n=e.get("grant_type"),a=e.get("code"),t=e.get("redirect_uri")||void 0,s=e.get("code_verifier")||void 0;if(n!=="authorization_code"||!a)return o(r,{error:"invalid_request",error_description:"Invalid grant type or missing authorization code"},400);try{const i=await N(a);if(t&&t!==i.redirect_uri)return o(r,{error:"invalid_grant",error_description:"redirect_uri mismatch"},400);if(i.code_challenge){if(!s)return o(r,{error:"invalid_grant",error_description:"code_verifier required"},400);if(!F(i.code_challenge,i.code_challenge_method,s))return o(r,{error:"invalid_grant",error_description:"code_verifier mismatch"},400)}if(I.OAUTH_CLIENT_ID&&i.client_id&&!P(i.client_id)&&i.client_id!==I.OAUTH_CLIENT_ID)return o(r,{error:"invalid_client",error_description:"Client mismatch"},400);const d=i.id_token;if(typeof d!="string"||d.length===0)return o(r,{error:"invalid_grant",error_description:"Missing id_token in authorization code"},400);let l=d;if(i.idp_access_token){const{payload:T,header:{kid:c}}=h.decode(d);l=await h.sign({...T,idp_access_token:i.idp_access_token},A,w.HS256,c)}return o(r,{access_token:l,token_type:"Bearer",expires_in:3600,scope:"openid profile email",id_token:d},200,{"Cache-Control":"no-store",Pragma:"no-cache"})}catch{return o(r,{error:"invalid_grant",error_description:"Invalid authorization code"},400)}}catch(e){const n=e instanceof Error?e.message:String(e);return o(r,{error:"server_error",error_description:"Failed to process token request",error_details:n},500)}}}function ue(){return async r=>{const e=new URL(r.req.url);let n=e.searchParams.get("context");if(!n&&e.pathname.startsWith(_(`${u.MCP_CALLBACK}/`))){const t=e.pathname.split("/");n=t[t.length-1]}if(!n)return m.sendMcpAuthorizationFailedMessage([{...f(null),error:"Missing context parameter",error_details:null}]),r.text("Missing context parameter",400);let a=null;try{const t=await G(n);if(a=t.mcpSessionId||null,!t.isMcpFlow||!t.originalRedirectUri)throw new Error("Invalid MCP context");const s=S(r,k.IDP_ID_TOKEN)||S(r,k.AUTHORIZATION),i=S(r,k.IDP_ACCESS_TOKEN);if(!s)return m.sendMcpAuthorizationFailedMessage([{...f(a),error:"Authentication required",error_details:null}]),r.text("Authentication required",401);const d=await $({idToken:s,idpAccessToken:i||void 0,clientId:t.mcpClientId||"",redirectUri:t.originalRedirectUri,codeChallenge:t.mcpCodeChallenge||void 0,codeChallengeMethod:t.mcpCodeChallengeMethod||void 0,ttlSec:600}),l=new URL(t.originalRedirectUri);return l.searchParams.set("code",d),t.mcpState&&l.searchParams.set("state",t.mcpState),m.sendMcpAuthorizationCompletedMessage([{...f(a),redirect_uri:t.originalRedirectUri||null}]),r.redirect(l.toString())}catch(t){const s=t instanceof Error?t.message:String(t),i=t instanceof Error?t.stack:String(t);return m.sendMcpAuthorizationFailedMessage([{...f(a),error:s,error_details:i}]),r.text(`Invalid MCP callback: ${s}`,400)}}}export{K as createMcpContextToken,le as mcpAuthorizationHandler,ue as mcpCallbackHandler,de as mcpDynamicClientRegistrationHandler,ce as mcpOAuthAuthorizationServerHandler,se as mcpOAuthProtectedResourceHandler,pe as mcpTokenPortalHandler,G as verifyAndParseMcpContextToken,F as verifyPkce};
@@ -1 +1 @@
1
- import{corsMiddleware as p}from"../../middleware/corsMiddleware.js";import{ServerRoutes as n}from"../../../../constants/api.js";import{EntitlementsProvider as r}from"../../../entitlements/entitlements-provider.js";import{withPathPrefix as t}from"@redocly/theme/core/utils";import{AGENT_CARD_SLUG as C,A2A_SLUG as a,MCP_SERVER_CARD_SLUG as l}from"../../../../constants/common.js";import{mcpOAuthProtectedResourceHandler as A,mcpOAuthAuthorizationServerHandler as d,mcpDynamicClientRegistrationHandler as s,mcpAuthorizationHandler as _,mcpTokenPortalHandler as R,mcpCallbackHandler as m}from"./mcp-oauth.js";import{mcpServerCardHandler as E}from"./mcp-server-card.js";import{agentCardHandler as P}from"./agent-card.js";import{a2aHandler as T}from"./a2a.js";function N(e,i){const o=i.getConfig().mcp,c=r.instance().canAccessFeature("mcp");!o?.hide&&!o?.docs?.hide&&c&&(e.get(t(l),E(i)),e.get(t(C),P(i)),e.use(t(a),p({allowMethods:["POST"],exposeHeaders:["Link"]})),e.post(t(a),T(i)),e.get(`${n.MCP_OAUTH_PROTECTED_RESOURCE}${t("/mcp")}`,A()),e.get(n.MCP_OAUTH_AUTHORIZATION_SERVER,d()),e.post(t(n.MCP_DYNAMIC_CLIENT_REGISTRATION),s()),e.get(t(n.MCP_AUTHORIZATION),_()),e.post(t(n.MCP_TOKEN_PORTAL),R()),e.get(t(n.MCP_CALLBACK),m()),e.get(t(`${n.MCP_CALLBACK}/*`),m()))}export{N as installMcpRoutes};
1
+ import{corsMiddleware as p}from"../../middleware/corsMiddleware.js";import{ServerRoutes as i}from"../../../../constants/api.js";import{EntitlementsProvider as C}from"../../../entitlements/entitlements-provider.js";import{withPathPrefix as n}from"@redocly/theme/core/utils";import{AGENT_CARD_SLUG as l,A2A_SLUG as m,MCP_SERVER_CARD_SLUG as A}from"../../../../constants/common.js";import{mcpOAuthProtectedResourceHandler as d,mcpOAuthAuthorizationServerHandler as r,mcpDynamicClientRegistrationHandler as _,mcpAuthorizationHandler as s,mcpTokenPortalHandler as R,mcpCallbackHandler as o}from"./mcp-oauth.js";import{mcpServerCardHandler as E}from"./mcp-server-card.js";import{agentCardHandler as P}from"./agent-card.js";import{a2aHandler as T}from"./a2a.js";function N(e,t){const a=t.getConfig().mcp,c=C.instance().canAccessFeature("mcp");!a?.hide&&!a?.docs?.hide&&c&&(e.get(n(A),E(t)),e.get(n(l),P(t)),e.use(n(m),p({allowMethods:["POST"],exposeHeaders:["Link"]})),e.post(n(m),T(t)),e.get(`${i.MCP_OAUTH_PROTECTED_RESOURCE}${n("/mcp")}`,d(t)),e.get(i.MCP_OAUTH_AUTHORIZATION_SERVER,r(t)),e.post(n(i.MCP_DYNAMIC_CLIENT_REGISTRATION),_(t)),e.get(n(i.MCP_AUTHORIZATION),s(t)),e.post(n(i.MCP_TOKEN_PORTAL),R()),e.get(n(i.MCP_CALLBACK),o()),e.get(n(`${i.MCP_CALLBACK}/*`),o()))}export{N as installMcpRoutes};
@@ -1 +1 @@
1
- import{withPathPrefix as p}from"@redocly/theme/core/utils";import{SEMANTIC_SEARCH_API_URL as r}from"../../constants/common.js";function l(u){return async e=>{if(!r)return e.newResponse(null,404);const n=e.get("auth"),o={"Content-Type":"application/json"};n.idpAccessToken&&(o.Cookie=`accessToken=${n.idpAccessToken}`);const a=await e.req.json();try{const t=await fetch(r,{method:"POST",body:JSON.stringify(a),headers:o});if(t.ok){const c=(await t.json()).documents.map(s=>({...s,url:p(s.url)}));return e.json(c)}const i=await t.text();return e.newResponse(i,t.status,{"Content-Type":"application/json"})}catch(t){return e.json({error:"Failed to fetch semantic search results",details:String(t)},500)}}}export{l as semanticSearchHandler};
1
+ import{withPathPrefix as p}from"@redocly/theme/core/utils";import{SEMANTIC_SEARCH_API_URL as s}from"../../constants/common.js";import{fetchWithClient as u}from"../../utils/fetch-with-client.js";function T(d){return async t=>{if(!s)return t.newResponse(null,404);const n=t.get("auth"),o={"Content-Type":"application/json"};n.idpAccessToken&&(o.Cookie=`accessToken=${n.idpAccessToken}`);const a=await t.req.json();try{const e=await u(s,{method:"POST",body:JSON.stringify(a),headers:o});if(e.ok){const c=(await e.json()).documents.map(r=>({...r,url:p(r.url)}));return t.json(c)}const i=await e.text();return t.newResponse(i,e.status,{"Content-Type":"application/json"})}catch(e){return t.json({error:"Failed to fetch semantic search results",details:String(e)},500)}}}export{T as semanticSearchHandler};
@@ -1 +1 @@
1
- import{getAllowedEnvs as r}from"../utils/envs/get-api-route-allowed-env-variables.js";import{WorkerPool as o}from"./worker-pool.js";const p="executeApiRoute",e=3,i=new o({workerScript:"./api-routes-worker",minWorkers:1,maxWorkers:e,workerType:"process",forkOpts:{stdio:"inherit"},resolveForkEnv:r,lazy:!0});export{p as API_ROUTES_WORKER_KEY,i as apiRoutesWorkers};
1
+ import{getAllowedEnvs as r}from"../utils/envs/get-api-route-allowed-env-variables.js";import{WorkerPool as o}from"./worker-pool.js";const p="executeApiRoute",e=3,i=new o({workerScript:"./api-routes-worker",minWorkers:1,maxWorkers:e,workerType:"process",forkOpts:{stdio:"inherit"},resolveForkEnv:r,lazy:!0,sandbox:!0});export{p as API_ROUTES_WORKER_KEY,i as apiRoutesWorkers};
@@ -1 +1 @@
1
- import{getAllowedEnvs as o}from"../utils/envs/get-api-route-allowed-env-variables.js";import{WorkerPool as r}from"./worker-pool.js";const p="executeMcpTool",c="isMcpToolAvailable",e=5,l=new r({workerScript:"./mcp-tool-worker",minWorkers:1,maxWorkers:e,workerType:"process",forkOpts:{stdio:"inherit"},resolveForkEnv:o,lazy:!0});export{c as MCP_TOOL_IS_AVAILABLE_KEY,p as MCP_TOOL_WORKER_KEY,l as mcpToolWorkers};
1
+ import{getAllowedEnvs as o}from"../utils/envs/get-api-route-allowed-env-variables.js";import{WorkerPool as r}from"./worker-pool.js";const p="executeMcpTool",c="isMcpToolAvailable",e=5,l=new r({workerScript:"./mcp-tool-worker",minWorkers:1,maxWorkers:e,workerType:"process",forkOpts:{stdio:"inherit"},resolveForkEnv:o,lazy:!0,sandbox:!0});export{c as MCP_TOOL_IS_AVAILABLE_KEY,p as MCP_TOOL_WORKER_KEY,l as mcpToolWorkers};
@@ -3,6 +3,7 @@ import type { WorkerTypeMapping, WorkerParams, WorkerResponse } from './types.js
3
3
  export type WorkerPoolConfig = {
4
4
  workerScript: string;
5
5
  lazy: boolean;
6
+ sandbox?: boolean;
6
7
  resolveForkEnv?: () => Record<string, string | undefined>;
7
8
  } & WorkerPoolOptions;
8
9
  type RegisteredWorkers = keyof WorkerTypeMapping;
@@ -1 +1 @@
1
- import n from"workerpool";import{basename as a,dirname as s,resolve as l}from"node:path";import{fileURLToPath as h}from"node:url";import{envConfig as m}from"../config/env-config.js";class v{#t=null;#r;constructor(o){this.#r=o,this.#r.lazy||(this.#t=this.#o())}#o(){const{workerScript:o,lazy:t,resolveForkEnv:r,...e}=this.#r;let i=this.#e(o);return r&&e.forkOpts&&(e.forkOpts={...e.forkOpts,env:r()}),n.pool(i,{...e})}#e(o){let t=o;if(!t.endsWith(".js")&&!t.endsWith(".mjs")&&(t=`${t}.${m.isDevelopMode?"js":"mjs"}`),!t.startsWith("."))return t;const r=s(h(import.meta.url)),e=a(r)==="chunks"?s(r):r;return l(e,t)}#i(){return this.#t||(this.#t=this.#o()),this.#t}async exec(o,t,r){let i=this.#i().exec(o,t);return r?.timeout&&(i=i.timeout(r.timeout)),await i}async terminate(){this.#t&&(await this.#t.terminate(),this.#t=null)}}export{v as WorkerPool};
1
+ import l from"workerpool";import{mkdirSync as m}from"node:fs";import{basename as u,dirname as c,resolve as f}from"node:path";import{fileURLToPath as p}from"node:url";import{envConfig as n}from"../config/env-config.js";import{SandboxContext as v}from"../utils/sandbox-context.js";const h=(()=>{const o=c(p(import.meta.url));return u(o)==="chunks"?c(o):o})();class D{#t=null;#r;constructor(r){this.#r=r,this.#r.lazy||(this.#t=this.#e())}#e(){const{workerScript:r,lazy:t,sandbox:i,resolveForkEnv:s,...e}=this.#r,a=this.#s(r);return s&&e.forkOpts&&(e.forkOpts={...e.forkOpts,env:s()}),this.#o(i)?l.pool(a,{...e,forkOpts:this.#i(e.forkOpts)}):l.pool(a,{...e})}#o(r){return!!r&&n.isReunite&&n.REDOCLY_DISABLE_API_FUNCTION_SANDBOX!==!0}#i(r){const t=v.resolve(h);try{m(t.tmpDir,{recursive:!0})}catch{}return{...r,execArgv:[...t.execArgv,...r?.execArgv??[]],env:{...r?.env,...t.env}}}#s(r){let t=r;return!t.endsWith(".js")&&!t.endsWith(".mjs")&&(t=`${t}.${n.isDevelopMode?"js":"mjs"}`),t.startsWith(".")?f(h,t):t}#n(){return this.#t||(this.#t=this.#e()),this.#t}async exec(r,t,i){let e=this.#n().exec(r,t);return i?.timeout&&(e=e.timeout(i.timeout)),await e}async terminate(){this.#t&&(await this.#t.terminate(),this.#t=null)}}export{D as WorkerPool};
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@redocly/realm",
3
- "version": "0.136.0-next.0",
3
+ "version": "0.136.0-next.2",
4
4
  "description": "",
5
5
  "type": "module",
6
6
  "bin": {
@@ -21,7 +21,7 @@
21
21
  "@jitl/quickjs-singlefile-mjs-release-sync": "0.32.0",
22
22
  "@libsql/client": "0.17.3",
23
23
  "@markdoc/markdoc": "0.5.2",
24
- "@opentelemetry/api": "1.9.0",
24
+ "@opentelemetry/api": "1.9.1",
25
25
  "@opentelemetry/context-zone": "2.0.1",
26
26
  "@opentelemetry/core": "2.8.0",
27
27
  "@opentelemetry/exporter-trace-otlp-http": "0.219.0",
@@ -93,14 +93,14 @@
93
93
  "xpath": "0.0.34",
94
94
  "yaml-ast-parser": "0.0.43",
95
95
  "zod": "^3.25.76",
96
- "@redocly/asyncapi-docs": "1.13.0-next.0",
96
+ "@redocly/asyncapi-docs": "1.13.0-next.2",
97
97
  "@redocly/config": "0.52.0",
98
- "@redocly/graphql-docs": "1.13.0-next.0",
99
- "@redocly/openapi-docs": "3.24.0-next.0",
98
+ "@redocly/graphql-docs": "1.13.0-next.2",
99
+ "@redocly/openapi-docs": "3.24.0-next.2",
100
100
  "@redocly/portal-legacy-ui": "0.18.0",
101
- "@redocly/portal-plugin-mock-server": "0.21.0-next.0",
101
+ "@redocly/portal-plugin-mock-server": "0.21.0-next.2",
102
102
  "@redocly/realm-asyncapi-sdk": "0.14.0-next.0",
103
- "@redocly/theme": "0.68.0-next.0"
103
+ "@redocly/theme": "0.68.0-next.1"
104
104
  },
105
105
  "peerDependencies": {
106
106
  "react": "^19.2.4",
@@ -1,3 +0,0 @@
1
- export declare const SANDBOX_POLYFILLS = "\n const URLSearchParams = class URLSearchParams {\n constructor(init) {\n this._params = [];\n if (typeof init === 'string') {\n const str = init.startsWith('?') ? init.slice(1) : init;\n for (const pair of (str ? str.split('&') : [])) {\n const eq = pair.indexOf('=');\n const k = eq >= 0 ? pair.slice(0, eq) : pair;\n const v = eq >= 0 ? pair.slice(eq + 1) : '';\n if (k) this._params.push([decodeURIComponent(k), decodeURIComponent(v)]);\n }\n } else if (init != null && typeof init === 'object') {\n for (const [k, v] of Object.entries(init)) this._params.push([k, String(v)]);\n }\n }\n append(key, value) { this._params.push([String(key), String(value)]); }\n set(key, value) {\n const k = String(key), v = String(value);\n this._params = this._params.filter(([pk]) => pk !== k);\n this._params.push([k, v]);\n }\n get(key) { return (this._params.find(([k]) => k === String(key)) ?? [undefined, null])[1]; }\n getAll(key) { return this._params.filter(([k]) => k === String(key)).map(([, v]) => v); }\n has(key) { return this._params.some(([k]) => k === String(key)); }\n delete(key) { this._params = this._params.filter(([k]) => k !== String(key)); }\n entries() { return this._params.values(); }\n [Symbol.iterator]() { return this._params.values(); }\n toString() {\n return this._params\n .map(([k, v]) => encodeURIComponent(k) + '=' + encodeURIComponent(v))\n .join('&');\n }\n };\n globalThis.URLSearchParams = URLSearchParams;\n\n const FormData = class FormData {\n constructor() { this._fields = []; }\n append(key, value) { this._fields.push([String(key), String(value)]); }\n set(key, value) {\n const k = String(key), v = String(value);\n this._fields = this._fields.filter(([fk]) => fk !== k);\n this._fields.push([k, v]);\n }\n get(key) { return (this._fields.find(([k]) => k === String(key)) ?? [undefined, null])[1]; }\n has(key) { return this._fields.some(([k]) => k === String(key)); }\n delete(key) { this._fields = this._fields.filter(([k]) => k !== String(key)); }\n entries() { return this._fields.values(); }\n [Symbol.iterator]() { return this._fields.values(); }\n };\n globalThis.FormData = FormData;\n";
2
- export declare const SANDBOX_FETCH_SETUP = "\n const hostFetch = globalThis.__fetch;\n delete globalThis.__fetch;\n globalThis.fetch = (url, init = {}) => {\n let body;\n let autoContentType;\n if (init.body instanceof URLSearchParams) {\n body = init.body.toString();\n autoContentType = 'application/x-www-form-urlencoded';\n } else if (init.body instanceof FormData) {\n const boundary = '----FormBoundary' + Math.random().toString(36).slice(2);\n body = init.body._fields\n .map(([k, v]) =>\n '--' + boundary + '\\r\\nContent-Disposition: form-data; name=\"' + k + '\"\\r\\n\\r\\n' + v,\n )\n .join('\\r\\n') + '\\r\\n--' + boundary + '--';\n autoContentType = 'multipart/form-data; boundary=' + boundary;\n } else if (init.body != null && typeof init.body !== 'string') {\n throw new TypeError('fetch body must be a string, URLSearchParams, or FormData');\n } else {\n body = init.body ?? undefined;\n }\n const callerHeaders = init.headers;\n const hasContentType = callerHeaders != null && Object.keys(callerHeaders).some(\n (k) => k.toLowerCase() === 'content-type',\n );\n const headers = autoContentType && !hasContentType\n ? Object.assign({ 'content-type': autoContentType }, callerHeaders)\n : callerHeaders;\n return hostFetch(JSON.stringify({ url: String(url), method: init.method, headers, body }))\n .then((json) => {\n const res = JSON.parse(json);\n const resHeaders = res.headers || {};\n return {\n status: res.status,\n statusText: res.statusText,\n ok: res.ok,\n headers: {\n get: (name) => {\n const value = resHeaders[String(name).toLowerCase()];\n return value === undefined ? null : value;\n },\n },\n json: () => Promise.resolve(JSON.parse(res.bodyText)),\n text: () => Promise.resolve(res.bodyText),\n };\n });\n };\n";
3
- //# sourceMappingURL=sandbox-polyfills.d.ts.map
@@ -1,3 +0,0 @@
1
- import type { RunInSandboxOptions, SandboxResult, SandboxTool } from '../types.js';
2
- export declare function runInSandbox(code: string, tools: Record<string, SandboxTool>, options?: RunInSandboxOptions): Promise<SandboxResult>;
3
- //# sourceMappingURL=sandbox.d.ts.map
@@ -1,15 +0,0 @@
1
- import P from"@jitl/quickjs-singlefile-mjs-release-sync";import{newQuickJSWASMModuleFromVariant as A,shouldInterruptAfterDeadline as j}from"quickjs-emscripten-core";import{SANDBOX_FETCH_SETUP as J,SANDBOX_POLYFILLS as N}from"./sandbox-polyfills.js";const B=1e4,C=64*1024*1024,L=256*1024,y=100,$=1024*1024,I=1024*1024,R=1024*1024;let v;function x(){return v??=A(P)}function F(t,r){const u=t.map(l=>`${JSON.stringify(l)}: (input) => invoke(${JSON.stringify(l)}, JSON.stringify(input ?? {})).then(JSON.parse)`).join(", ");return`(() => {
2
- const invoke = globalThis.__invoke;
3
- delete globalThis.__invoke;${r?N+J:""}
4
- const tools = Object.freeze({ ${u} });
5
- globalThis.tools = new Proxy(tools, {
6
- get(target, prop) {
7
- if (typeof prop !== 'string' || prop in target || prop === 'then' || prop === 'toJSON') {
8
- return target[prop];
9
- }
10
- throw new TypeError('Unknown tool "' + prop + '". Available tools: ' + Object.keys(target).join(', '));
11
- },
12
- });
13
- })();`}async function H(t,r,u={}){const{timeoutMs:i=B,hostFetch:l}=u,T=new Set;let b=0,_=0;const e=(await x()).newContext();let c,h=()=>{};const k=new Promise((o,a)=>{h=()=>a(new Error(`Execution timed out after ${i}ms`))}),O=()=>e.runtime.setInterruptHandler(j(Date.now()+i)),w=()=>{clearTimeout(c),c=setTimeout(h,i)},E=(o,a,g,d)=>{X(e,o,(...p)=>{const f=p.map(s=>e.getString(s)),n=e.newPromise();return T.add(n),_++,clearTimeout(c),Promise.resolve().then(()=>a(f)).then(s=>{if(!e.alive)return;const S=JSON.stringify(s??null);S.length>g?e.newError(d(S.length)).consume(n.reject):e.newString(S).consume(n.resolve)},s=>{e.alive&&e.newError(M(s)).consume(n.reject)}),n.settled.then(()=>{e.alive&&(O(),--_===0&&w(),e.runtime.executePendingJobs())}),n.handle})};let m;try{e.runtime.setMemoryLimit(C),e.runtime.setMaxStackSize(L),O(),w();const o=()=>{if(++b>y)throw new Error(`Exceeded the limit of ${y} tool calls`)};E("__invoke",([n,s])=>(o(),r[n](JSON.parse(s))),$,n=>`Tool output is too large (${n} bytes); request narrower data instead`),l&&E("__fetch",([n])=>(o(),l(JSON.parse(n))),I,n=>`Response is too large (${n} bytes); request less data`),Y(e,F(Object.keys(r),!!l)),m=e.unwrapResult(e.evalCode(`(async () => {
14
- ${t}
15
- })()`));const a=e.resolvePromise(m);e.runtime.executePendingJobs();const g=await Promise.race([a,k]),d=e.unwrapResult(g),p=e.dump(d);d.dispose();const f=JSON.stringify(p??null).length;return f>R?{error:`Result is too large (${f} bytes); return less data`}:{result:p}}catch(o){return{error:M(o)}}finally{clearTimeout(c),m?.dispose();for(const o of T)o.alive&&o.dispose();try{e.dispose()}catch{v=void 0}}}function X(t,r,u){t.newFunction(r,u).consume(i=>t.setProp(t.global,r,i))}function Y(t,r){t.unwrapResult(t.evalCode(r)).dispose()}function M(t){return t instanceof Error?t.message:String(t)}export{H as runInSandbox};