@redocly/redoc-revel 0.133.0-next.4 → 0.133.0-next.5
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +25 -0
- package/dist/client/app/seo/SeoTags.js +1 -1
- package/dist/client/app/seo/hooks/useHreflangLinks.d.ts +10 -0
- package/dist/client/app/seo/hooks/useHreflangLinks.js +1 -0
- package/dist/client/app/utils/loadAndNavigate.js +1 -1
- package/dist/client/utils/auth/is-auth-route-path.d.ts +11 -0
- package/dist/client/utils/auth/is-auth-route-path.js +1 -0
- package/dist/server/plugins/catalog-entities/database/repositories/catalog-entities-repository.js +1 -1
- package/dist/server/plugins/catalog-entities/database/repositories/entities/entities-write-repository.js +1 -1
- package/dist/server/plugins/catalog-entities/database/repositories/relations/relations-write-repository.js +1 -1
- package/dist/server/plugins/mcp/auth/auth-handlers.js +1 -1
- package/dist/server/web-server/auth.d.ts +7 -2
- package/dist/server/web-server/auth.js +2 -2
- package/dist/server/web-server/routes/auth.js +1 -1
- package/dist/server/web-server/routes/helpers/resolve-ui-locales-for-idp-login.d.ts +11 -0
- package/dist/server/web-server/routes/helpers/resolve-ui-locales-for-idp-login.js +1 -0
- package/dist/server/web-server/routes/mcp-routes/mcp-client-metadata.d.ts +14 -0
- package/dist/server/web-server/routes/mcp-routes/mcp-client-metadata.js +1 -0
- package/dist/server/web-server/routes/mcp-routes/mcp-oauth.d.ts +3 -0
- package/dist/server/web-server/routes/mcp-routes/mcp-oauth.js +1 -1
- package/package.json +8 -8
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,30 @@
|
|
|
1
1
|
# @redocly/redoc-revel
|
|
2
2
|
|
|
3
|
+
## 0.133.0-next.5
|
|
4
|
+
|
|
5
|
+
### Minor Changes
|
|
6
|
+
|
|
7
|
+
- 0858f54ae5e: Added Client ID Metadata Document (CIMD) support to the MCP OAuth flow.
|
|
8
|
+
- 7e407eeb7c9: Added `hreflang` alternate links for translated pages.
|
|
9
|
+
- 7e407eeb7c9: Improved language picker accessibility by converting menu options to links.
|
|
10
|
+
- 49d39a04865: Fixed an issue where the login page didn't respect language preferences selected by the user.
|
|
11
|
+
|
|
12
|
+
### Patch Changes
|
|
13
|
+
|
|
14
|
+
- 49d39a04865: Fixed 404 error when opening login from locale-prefixed URLs.
|
|
15
|
+
- 9f60402b867: Fixed an issue where navigating on a code walkthrough page prevented automatic scrolling to the active step.
|
|
16
|
+
- Updated dependencies [49d39a04865]
|
|
17
|
+
- Updated dependencies [7e407eeb7c9]
|
|
18
|
+
- Updated dependencies [7e407eeb7c9]
|
|
19
|
+
- Updated dependencies [59198fb803c]
|
|
20
|
+
- Updated dependencies [9f60402b867]
|
|
21
|
+
- @redocly/theme@0.65.0-next.5
|
|
22
|
+
- @redocly/realm-asyncapi-sdk@0.11.0-next.3
|
|
23
|
+
- @redocly/asyncapi-docs@1.10.0-next.5
|
|
24
|
+
- @redocly/graphql-docs@1.10.0-next.5
|
|
25
|
+
- @redocly/openapi-docs@3.21.0-next.5
|
|
26
|
+
- @redocly/portal-plugin-mock-server@0.18.0-next.5
|
|
27
|
+
|
|
3
28
|
## 0.133.0-next.4
|
|
4
29
|
|
|
5
30
|
### Minor Changes
|
|
@@ -1 +1 @@
|
|
|
1
|
-
import
|
|
1
|
+
import n from"react";import{Helmet as u}from"@dr.pogodin/react-helmet";import{combineUrls as p}from"@redocly/theme/core/utils";import{isTruthy as g}from"../../../utils/guards/is-truthy";import{usePageVersions as k}from"../../providers/page-data/hooks";import{getMetaTagsAttributes as d}from"../utils/get-meta-tags-attributes";import{useHreflangLinks as E}from"./hooks/useHreflangLinks";function U(o){const{seo:e,slug:i}=o,{versions:s}=k(),r=e?.siteUrl,{localeAlternates:a}=E(r);if(!e)return null;const m=d(e),c=[e.title,e.projectTitle].filter(g).join(" | "),f=s.find(t=>t.default)?.link||i;return n.createElement(u,null,n.createElement("title",null,c),r?n.createElement("link",{rel:"canonical",href:p(r,f)}):null,a.map(({hrefLang:t,href:l})=>n.createElement("link",{key:t,rel:"alternate",hrefLang:t,href:l})),e.jsonLd?n.createElement("script",{type:"application/ld+json"},JSON.stringify(e.jsonLd)):null,m.map((t,l)=>n.createElement("meta",{key:l,...t})))}export{U as SeoTags};
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
type HreflangLink = {
|
|
2
|
+
hrefLang: string;
|
|
3
|
+
href: string;
|
|
4
|
+
};
|
|
5
|
+
type HreflangLinksResult = {
|
|
6
|
+
localeAlternates: HreflangLink[];
|
|
7
|
+
};
|
|
8
|
+
export declare function useHreflangLinks(siteUrl?: string): HreflangLinksResult;
|
|
9
|
+
export {};
|
|
10
|
+
//# sourceMappingURL=useHreflangLinks.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
import{useMemo as h}from"react";import{useLocation as l}from"react-router-dom";import{combineUrls as i,getPathnameForLocale as m,withPathPrefix as c,withoutPathPrefix as u}from"@redocly/theme/core/utils";import{DEFAULT_LOCALE_PLACEHOLDER as p}from"../../../../constants/common";import{useL10nConfig as L}from"../../hooks/useL10nConfig";function x(e){const n=l(),{locales:o,defaultLocale:t}=L();return h(()=>{if(!e)return{localeAlternates:[]};const f=u(n.pathname),a=o.filter(r=>r.code!==p).map(r=>({hrefLang:r.code.replaceAll("_","-"),href:i(e,c(m(f,t,r.code,o)))}));return a.length>0&&a.push({hrefLang:"x-default",href:i(e,c(m(f,t,t,o)))}),{localeAlternates:a}},[t,o,n.pathname,e])}export{x as useHreflangLinks};
|
|
@@ -1 +1 @@
|
|
|
1
|
-
import{
|
|
1
|
+
import{isLocalLink as f}from"../../../utils/path/is-local-link.js";import{isAuthRoutePath as m}from"../../utils/auth/is-auth-route-path.js";import{withLoadProgress as u}from"./withLoadProgress";import{scrollToAnchor as p}from"./scroll-to-anchor";let s;async function A({navigate:i,to:o,origin:a="browser",options:c}){o===""&&(o="/"),s=o;const{pathname:n,hash:d,search:t}=new URL(o,window.location.origin+window.location.pathname),r=decodeURIComponent(d),l=decodeURIComponent(window.location.hash);if(m(n)){window.location.href=o;return}const w=window.__LOADER,e=await u(w.tryLoad(n,void 0,t));if(e?.redirectTo){if(!f(e.redirectTo)){window.location.href=e.redirectTo;return}return A({navigate:i,to:e.redirectTo,origin:a,options:c})}if(e&&s===o){if((n!==window.location.pathname||t!==window.location.search||r!==l)&&i({pathname:n,search:t,hash:r},{...c,state:{origin:a},unstable_flushSync:!0}),e.props?.disableAutoScroll)return;requestAnimationFrame(()=>{if(r){const h=r.slice(1);p(h)}else window.scrollTo(0,0)})}}export{A as loadAndNavigate};
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Returns true if the pathname targets Realm reserved auth URLs (`/_auth/*`).
|
|
3
|
+
*
|
|
4
|
+
* @param pathname - Full pathname (e.g. from URL), optionally with path prefix.
|
|
5
|
+
* @returns `true` if the first path segment, after `withoutPathPrefix`, is `_auth`.
|
|
6
|
+
* @example
|
|
7
|
+
* isAuthRoutePath('/_auth/idp-login'); // true
|
|
8
|
+
* isAuthRoutePath('/es/_auth/idp-login'); // false
|
|
9
|
+
*/
|
|
10
|
+
export declare function isAuthRoutePath(pathname: string): boolean;
|
|
11
|
+
//# sourceMappingURL=is-auth-route-path.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
import{withoutPathPrefix as n}from"@redocly/theme/core/utils";const i="_auth";function s(e){const t=n(e);return(t.startsWith("/")?t:`/${t}`).split("/").filter(Boolean)}function a(e){return s(e)[0]===i}export{a as isAuthRoutePath};
|
package/dist/server/plugins/catalog-entities/database/repositories/catalog-entities-repository.js
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
import{logger as n}from"../../../../tools/notifiers/logger.js";import{telemetryTraceStep as s}from"../../../../telemetry/helpers/trace-step.js";import{BaseRepository as a}from"../../../../providers/database/base-repository.js";import{DatabaseConnectionFactory as c}from"../../../../providers/database/database-connection-factory.js";import{RelationsReadRepository as o}from"./relations/relations-read-repository.js";import{RevisionRepository as f}from"./common/revision-repository.js";import{BffEntitiesReadRepository as m}from"./bffEntities/bff-entities-read-repository.js";import{EntitiesReadRepository as l}from"./entities/entities-read-repository.js";import{EntitiesWriteRepository as p}from"./entities/entities-write-repository.js";import{RelationsWriteRepository as w}from"./relations/relations-write-repository.js";import{FiltersRepository as h}from"./common/filters-repository.js";class e extends a{static#t;entitiesRead;entitiesWrite;relationsRead;relationsWrite;bffEntitiesRead;filters;revisions;get transactionsManager(){return this.databaseClient.transactionsManager}constructor(t){super(t),this.revisions=new f(t.client),this.bffEntitiesRead=new m(t.client),this.relationsRead=new o(t.client),this.entitiesRead=new l(t.client),this.entitiesWrite=new p(t.client,this.organizationId,this.projectId),this.relationsWrite=new w(t.client,this.organizationId,this.projectId),this.filters=new h(t.client)}async sync(){return s("catalog_entities.
|
|
1
|
+
import{logger as n}from"../../../../tools/notifiers/logger.js";import{telemetryTraceStep as s}from"../../../../telemetry/helpers/trace-step.js";import{BaseRepository as a}from"../../../../providers/database/base-repository.js";import{DatabaseConnectionFactory as c}from"../../../../providers/database/database-connection-factory.js";import{RelationsReadRepository as o}from"./relations/relations-read-repository.js";import{RevisionRepository as f}from"./common/revision-repository.js";import{BffEntitiesReadRepository as m}from"./bffEntities/bff-entities-read-repository.js";import{EntitiesReadRepository as l}from"./entities/entities-read-repository.js";import{EntitiesWriteRepository as p}from"./entities/entities-write-repository.js";import{RelationsWriteRepository as w}from"./relations/relations-write-repository.js";import{FiltersRepository as h}from"./common/filters-repository.js";class e extends a{static#t;entitiesRead;entitiesWrite;relationsRead;relationsWrite;bffEntitiesRead;filters;revisions;get transactionsManager(){return this.databaseClient.transactionsManager}constructor(t){super(t),this.revisions=new f(t.client),this.bffEntitiesRead=new m(t.client),this.relationsRead=new o(t.client),this.entitiesRead=new l(t.client),this.entitiesWrite=new p(t.client,this.organizationId,this.projectId),this.relationsWrite=new w(t.client,this.organizationId,this.projectId),this.filters=new h(t.client)}async sync(){return s("catalog_entities.repository.sync",async()=>{await this.databaseClient.sync()})}static async#e(t){const r=await c.create(t);if(!r)throw new Error("Failed to create db connection for catalog entities repository");return new e(r)}static async getInstance(t){return await s("catalog_entities.repository.get_instance",async r=>{if(e.#t)return e.#t;try{return e.#t=await e.#e(t),e.#t}catch(i){throw n.error("Error creating db connection for catalog entities repository",i),r?.error(i),e.#t=null,i}})}static async recreateInstance(t){const r=e.#t,i=await e.#e(t);return e.#t=i,r&&await r.close(),i}static async resetInstance(){const t=e.#t;e.#t=null,t&&await t.close()}}export{e as CatalogEntitiesRepository};
|
|
@@ -1 +1 @@
|
|
|
1
|
-
import{and as v,eq as c,isNull as R,or as
|
|
1
|
+
import{and as v,eq as c,isNull as R,or as x,sql as I}from"drizzle-orm";import{VERSION_NOT_SPECIFIED as V}from"@redocly/theme/core/constants";import{sha1 as F}from"../../../../../utils/crypto/sha1.js";import{logger as y}from"../../../../../tools/notifiers/logger.js";import{envConfig as K}from"../../../../../config/env-config.js";import{entitiesTable as i}from"../../../../../providers/database/databases/sqlite-db/schemas/entities-table.js";import{convertFilterToWhereCondition as O}from"../../../../../providers/database/pagination/filter.js";import{entitiesRelationsTable as a}from"../../../../../providers/database/databases/sqlite-db/schemas/entities-relations-table.js";import{promiseMapLimit as E}from"../../../../../utils/async/promise-map-limit.js";import{RevisionRepository as z}from"../common/revision-repository.js";import{VersionRepository as H}from"../common/version-repository.js";import{EntityAttributesWriteRepository as P}from"../entityAttributes/entity-attributes-write-repository.js";import{createEntityDbRecord as W}from"../../mappers/create-entity-db-record.js";import{createEntityReadModel as C}from"../../mappers/create-entity-read-model.js";import{createEntityRelationDbRecordFromFileSchema as j}from"../../mappers/create-entity-relation-db-record-from-file-schema.js";import{createEntityRelationDbRecordFromDto as G}from"../../mappers/create-entity-relation-db-record-from-dto.js";import{EntitiesReadRepository as M}from"./entities-read-repository.js";import{RelationsReadRepository as $}from"../relations/relations-read-repository.js";import{RelationsWriteRepository as b}from"../relations/relations-write-repository.js";const m=15;class he{#e;#t;#n;#o;#r;#i;#a;#u;#s;constructor(e,t,s){this.#e=e,this.#r=t,this.#i=s,this.#t=new z(e),this.#n=new H(e),this.#o=new P(e),this.#a=new M(e),this.#u=new $(e),this.#s=new b(e,t,s)}async createEntity({entity:e,source:t,fileHash:s,sourceFile:n,isRootEntity:r,isDeleted:o,rbacTeams:u,errorOnSkip:h=!1,revision:f}){const{relations:l=[],...p}=e,d=F(JSON.stringify(p)),g=e.version??V,D=f??new Date().toISOString(),L=await this.#t.shouldSkipRevisionCreation(e.key,g,d,r,o);if(Array.isArray(u)&&await this.#o.upsertEntityAttributes({entityKey:e.key,rbacTeams:u,organizationId:this.#r,projectId:this.#i}),L){if(h)throw new Error("Entity validation failed: entity already exists");return null}const{shouldSetNewCurrentRevision:w,hasCurrentRevision:_}=await this.#t.getCurrentRevisionInfo({key:e.key,version:g,revision:D}),k=W({entity:{...e,revision:D,hash:d,isCurrent:w,isDefaultVersion:w,isDeleted:o,version:g},organizationId:this.#r,projectId:this.#i,source:t,sourceFile:n??null,fileHash:s??null}),{key:A,...T}=k;if(w&&_&&(await this.#t.markAllRevisionsAsNotCurrent(A),await this.#n.markAllVersionsAsNotDefault(A)),K.isDevelopMode&&!K.REDOCLY_INTERNAL_DEV)return await this.#l(k,l);const S=await this.#e.client.insert(i).values(k).onConflictDoUpdate({target:[i.key,i.source,i.revision,i.version],set:T}).returning();return S.length?(l&&await this.#s.createEntityRelations(l.map(N=>({...N,sourceKey:e.key,targetKey:N.key}))),C(S[0])):null}async updateEntity(e,t){try{const{shouldSetNewCurrentRevision:s,hasCurrentRevision:n}=await this.#t.getCurrentRevisionInfo({key:t.key,version:e.version??t.version??V,revision:t.revision});s&&n&&(await this.#t.markAllRevisionsAsNotCurrent(t.key),await this.#n.markAllVersionsAsNotDefault(t.key));const r=W({entity:{...t,...e,hash:F(JSON.stringify({...t,...e})),isCurrent:s,isDefaultVersion:s,createdAt:t.createdAt??void 0},organizationId:this.#r,projectId:this.#i,source:"remote",sourceFile:null,fileHash:null}),{key:o,source:u,scorecardsStatus:h,...f}=r,l=await this.#e.client.insert(i).values(r).onConflictDoUpdate({target:[i.key,i.source,i.revision,i.version],set:{...f,scorecardsStatus:I`CASE WHEN ${i.scorecardsStatus} = 'CALCULATING' THEN 'CANCELLED' ELSE 'OUTDATED' END`}}).returning();return l.length?C(l[0]):null}catch(s){return y.error("Error updating entity",s),null}}async setEntitiesAsOutdated(e){try{const t=O(e);await this.#e.client.update(i).set({scorecardsStatus:"OUTDATED"}).where(t)}catch(t){y.error("Error updating entities as outdated",t)}}async#l(e,t){const{key:s,source:n,version:r,isDefaultVersion:o,...u}=e,l=await this.#e.client.select({id:i.id}).from(i).where(v(c(i.key,s),r?c(i.version,r):R(i.version))).limit(1).get()!=null?await this.#e.client.update(i).set(u).where(v(c(i.key,s),r?c(i.version,r):R(i.version))).returning():await this.#e.client.insert(i).values(e).onConflictDoUpdate({target:[i.key,i.source,i.revision,i.version],set:u}).returning(),p=t?.map(d=>{const g=j({relation:d,sourceFile:e.sourceFile??"",fileHash:e.fileHash??"",sourceKey:e.key,sourceVersion:e.version??null,sourceRevision:e.revision??null,organizationId:this.#r,projectId:this.#i});return this.#e.client.insert(a).values(g).onConflictDoUpdate({target:[a.sourceKey,a.targetKey,a.sourceVersion,a.targetVersion,a.sourceRevision,a.targetRevision,a.sourceToTargetRelation],set:g}).run()})??[];return await E(p,m,async d=>d),C(l[0])}async softDeleteEntitiesWithRelations({filter:e,revision:t,fileHash:s}){const r={op:"AND",conditions:[e,{field:"is_deleted",operator:"equal",value:!1}]};for(;;){const o=await this.#a.getEntities({paginationParams:{filter:r,limit:500}});if(o.items.length===0)break;const u=await this.#c({entities:o.items,revision:t,fileHash:s});await this.#d(u,t)}}async deleteEntity(e){try{return await this.#s.deleteEntityRelationsOnEntityRemoval(e),await this.#e.client.delete(i).where(c(i.id,e.id)),await this.#t.ensureDefaultAndCurrentRevisionForKey(e.key),e.id}catch(t){return y.error("Error deleting entity",t),null}}async deleteEntities(e){try{const t=O(e);if(!t)return!1;const s=await this.#e.client.delete(i).where(t).returning({key:i.key,source:i.source,isCurrent:i.isCurrent,isDefaultVersion:i.isDefaultVersion,version:i.version});if(s.length===0)return!0;const n=s.reduce((r,o)=>((o.isCurrent||o.isDefaultVersion)&&r.add(o.key),r),new Set);if(n.size===0)return!0;await E(Array.from(n),m,async r=>this.#t.ensureDefaultAndCurrentRevisionForKey(r));for(const r of s)await this.#e.client.delete(a).where(x(v(c(a.sourceKey,r.key),...r.version?[c(a.sourceVersion,r.version)]:[R(a.sourceVersion)]),v(c(a.targetKey,r.key),...r.version?[c(a.targetVersion,r.version)]:[R(a.targetVersion)])));return!0}catch(t){return y.error("Error deleting entities",t),!1}}async updateEntityScorecardsStatus(e,t){try{return(await this.#e.client.update(i).set({scorecardsStatus:t}).where(c(i.id,e)).returning()).length>0}catch(s){return y.error("Error updating entity scorecards status",s),!1}}async updateEntityScorecardsStatusIfCalculating(e,t){try{return(await this.#e.client.update(i).set({scorecardsStatus:t}).where(I`${i.id} = ${e} AND ${i.scorecardsStatus} = 'CALCULATING'`).returning()).length>0}catch(s){return y.error("Error updating entity scorecards status if calculating",s),!1}}async#c({entities:e,revision:t,fileHash:s}){try{return(await E(e,m,async r=>{const o={type:r.type,key:r.key,title:r.title,summary:r.summary??void 0,tags:r.tags??void 0,metadata:r.metadata??void 0,git:r.git??void 0,contact:r.contact??void 0,links:r.links??void 0,version:r.version??void 0};return this.createEntity({entity:o,sourceFile:r.sourceFile??"",fileHash:s,isDeleted:!0,errorOnSkip:!1,source:r.source,revision:t})})).filter(r=>r!==null)}catch(n){return y.error("Error soft deleting entities",n),[]}}async#d(e,t){try{if(e.length===0)return!0;const s=await E(e,m,async n=>(await this.#u.getRelationsForEntity(n.key,n.version,t)).map(o=>{if(!o)return null;const u=o.direction,h=o.sourceToTargetRelation,f=o.targetKey,l=u==="outgoing"?n.key:f,p=u==="outgoing"?f:n.key,d=u==="outgoing"?h:h.startsWith("reverse:")?h.slice(8):h;return!d||!l||!p?null:G({type:d,sourceKey:l,targetKey:p,sourceVersion:n.version,targetVersion:n.version,sourceRevision:t,targetRevision:t,isDeleted:!0},this.#r,this.#i)}).filter(o=>o!==null));return await E(s.flat(),m,async n=>this.#s.upsertEntityRelation(n)),!0}catch(s){return y.error("Error soft deleting entity relations",s),!1}}}export{he as EntitiesWriteRepository};
|
|
@@ -1 +1 @@
|
|
|
1
|
-
import{and as a,eq as i,ne as
|
|
1
|
+
import{and as a,eq as i,ne as d,or as u,sql as g}from"drizzle-orm";import{entitiesRelationsTable as t}from"../../../../../providers/database/databases/sqlite-db/schemas/entities-relations-table.js";import{logger as c}from"../../../../../tools/notifiers/logger.js";import{entitiesTable as o}from"../../../../../providers/database/databases/sqlite-db/schemas/entities-table.js";import{promiseMapLimit as f}from"../../../../../utils/async/promise-map-limit.js";import{convertFilterToWhereCondition as v}from"../../../../../providers/database/pagination/filter.js";import{createEntityRelationReadModel as p}from"../../mappers/create-entity-relation-read-model.js";import{createEntityRelationDbRecordFromDto as w}from"../../mappers/create-entity-relation-db-record-from-dto.js";const E=15;class U{#e;#t;#r;constructor(r,e,n){this.#e=r,this.#t=e,this.#r=n}async createEntityRelation(r){try{const e=w(r,this.#t,this.#r),n=await this.upsertEntityRelation(e);return p(n)}catch(e){throw c.error("Error creating entity relation",e),e}}async createEntityRelations(r){return await f(r,E,async e=>this.createEntityRelation(e))}async upsertEntityRelation(r){if(!r)return null;try{const{sourceKey:e,targetKey:n,sourceVersion:h,targetVersion:s,sourceRevision:l,targetRevision:m,...R}=r,y=await this.#e.client.insert(t).values(r).onConflictDoUpdate({target:[t.sourceKey,t.targetKey,t.sourceVersion,t.targetVersion,t.sourceRevision,t.targetRevision,t.sourceToTargetRelation],set:R}).returning();return y?.length?y[0]:null}catch(e){return c.error("Error creating entity relation",e),null}}async deleteEntityRelation(r){try{return await this.#e.client.delete(t).where(i(t.id,r)),r}catch(e){return c.error("Error deleting entity relation",e),null}}async deleteEntitiesRelations(r){try{const e=v(r);return e?(await this.#e.client.delete(t).where(e),!0):!1}catch(e){return c.error("Error deleting entities relations",e),!1}}async deleteEntityRelationsOnEntityRemoval(r){const{key:e,version:n,revision:h}=r,s=n??"",l=h??"";if(((await this.#e.client.select({count:g`COUNT(*)`}).from(o).where(a(i(o.key,e),d(o.id,r.id))).get())?.count??0)===0){await this.#e.client.delete(t).where(u(i(t.sourceKey,e),i(t.targetKey,e)));return}if(((await this.#e.client.select({count:g`COUNT(*)`}).from(o).where(a(i(o.key,e),i(o.version,s),d(o.id,r.id))).get())?.count??0)===0){await this.#e.client.delete(t).where(u(a(i(t.sourceKey,e),i(t.sourceVersion,s)),a(i(t.targetKey,e),i(t.targetVersion,s))));return}await this.#e.client.delete(t).where(u(a(i(t.sourceKey,e),i(t.sourceVersion,s),i(t.sourceRevision,l)),a(i(t.targetKey,e),i(t.targetVersion,s),i(t.targetRevision,l))))}}export{U as RelationsWriteRepository};
|
|
@@ -1 +1 @@
|
|
|
1
|
-
import{extractTokenFromAuthHeader as u}from"../../../plugins/mcp/utils/jwt.js";import{getUserParamsFromCookies as d}from"../../../web-server/auth.js";import{DEFAULT_ANONYMOUS_VISITOR_TEAM as i,RBAC_ALL_OTHER_TEAMS as a,ServerRoutes as l}from"../../../../constants/common.js";import{withPathPrefix as h}from"@redocly/theme/core/utils";function c(e){return!e||typeof e!="object"||Object.keys(e).length===0?!1:!(e[i]&&e[i]!=="none"||e[a]&&e[a]!=="none")}function k(e,t){if(!t||Object.keys(t).length===0)return e;const r=t.content;if(r&&Object.keys(r).length>0&&Object.values(r).some(c))return!0;const
|
|
1
|
+
import{extractTokenFromAuthHeader as u}from"../../../plugins/mcp/utils/jwt.js";import{getUserParamsFromCookies as d}from"../../../web-server/auth.js";import{DEFAULT_ANONYMOUS_VISITOR_TEAM as i,RBAC_ALL_OTHER_TEAMS as a,ServerRoutes as l}from"../../../../constants/common.js";import{withPathPrefix as h}from"@redocly/theme/core/utils";function c(e){return!e||typeof e!="object"||Object.keys(e).length===0?!1:!(e[i]&&e[i]!=="none"||e[a]&&e[a]!=="none")}function k(e,t){if(!t||Object.keys(t).length===0)return e;const r=t.content;if(r&&Object.keys(r).length>0&&Object.values(r).some(c))return!0;const o=t.teamFoldersBaseRoles;return c(o)?!0:e}async function R(e,t){const r=e.headers.get("Authorization");if(!r)return{isAuthenticated:!1};const o=u(r),s=t?.config?.ssoDirect||{},n=o?await d(s,{authorization:o}):{};return o&&n&&n.isAuthenticated?{isAuthenticated:!0,isTokenValid:!0,currentUser:{teams:n.teams||[],email:n.email||"",claims:n,isAuthenticated:!0,idpAccessToken:n.idpAccessToken||void 0,idpId:n.idpId||void 0},accessToken:o}:{isAuthenticated:!1,isTokenValid:!1}}function O(e){return e=e.replace(/^http:\/\//,"https://"),new Response(JSON.stringify({error:"unauthorized",message:"Authentication required"}),{status:401,headers:{"Content-Type":"application/json","WWW-Authenticate":`Bearer resource_metadata="${e}${l.MCP_OAUTH_PROTECTED_RESOURCE}${h("/mcp")}"`,"Access-Control-Allow-Origin":"*"}})}function _(){return new Response(JSON.stringify({error:"invalid_token",message:"Invalid or expired token"}),{status:401,headers:{"Content-Type":"application/json","WWW-Authenticate":'Bearer error="invalid_token", error_description="Invalid or expired token"',"Access-Control-Allow-Origin":"*"}})}export{_ as constructInvalidTokenResponse,O as constructUnauthorizedResponse,R as handleMcpAuth,k as shouldHandleMcpAuth};
|
|
@@ -45,6 +45,7 @@ export declare function buildOidcLoginUrl(origin: string, { authorizationEndpoin
|
|
|
45
45
|
redirectUriOverride?: string;
|
|
46
46
|
sourceOverride?: 'portal' | 'mcp';
|
|
47
47
|
branchOverride?: string | undefined;
|
|
48
|
+
uiLocales?: string;
|
|
48
49
|
}): {
|
|
49
50
|
loginUrl?: string;
|
|
50
51
|
cookies?: Record<string, {
|
|
@@ -58,6 +59,8 @@ type McpAuthorizationCodePayload = {
|
|
|
58
59
|
redirect_uri: string;
|
|
59
60
|
id_token: string;
|
|
60
61
|
idp_access_token?: string;
|
|
62
|
+
code_challenge?: string;
|
|
63
|
+
code_challenge_method?: string;
|
|
61
64
|
iat: number;
|
|
62
65
|
exp: number;
|
|
63
66
|
};
|
|
@@ -66,6 +69,8 @@ export declare function createMcpAuthorizationCode(params: {
|
|
|
66
69
|
idpAccessToken?: string;
|
|
67
70
|
clientId: string;
|
|
68
71
|
redirectUri: string;
|
|
72
|
+
codeChallenge?: string;
|
|
73
|
+
codeChallengeMethod?: string;
|
|
69
74
|
ttlSec?: number;
|
|
70
75
|
}): Promise<string>;
|
|
71
76
|
export declare function verifyMcpAuthorizationCode(code: string): Promise<McpAuthorizationCodePayload>;
|
|
@@ -76,14 +81,14 @@ export declare function createMcpSessionResource(sessionId: string | null | unde
|
|
|
76
81
|
};
|
|
77
82
|
export declare function rewritePreviewAuthRedirectUri(redirectUri: string): string;
|
|
78
83
|
export declare function parsePreviewBranch(origin: string): string | undefined;
|
|
79
|
-
export declare function buildLoginUrl(idpLoginParams: AuthProviderLoginParams, redirectOrigin: string, redirectTo: string | null, inviteCode?: string): {
|
|
84
|
+
export declare function buildLoginUrl(idpLoginParams: AuthProviderLoginParams, redirectOrigin: string, redirectTo: string | null, inviteCode?: string, uiLocales?: string): {
|
|
80
85
|
loginUrl?: string;
|
|
81
86
|
cookies?: Record<string, {
|
|
82
87
|
value: string;
|
|
83
88
|
options: object;
|
|
84
89
|
}>;
|
|
85
90
|
};
|
|
86
|
-
export declare function buildSAML2LoginUrl(origin: string, idpLoginParams: Saml2LoginParams, redirectTo: string | null, inviteCode?: string): {
|
|
91
|
+
export declare function buildSAML2LoginUrl(origin: string, idpLoginParams: Saml2LoginParams, redirectTo: string | null, inviteCode?: string, uiLocales?: string): {
|
|
87
92
|
loginUrl: string;
|
|
88
93
|
};
|
|
89
94
|
export declare function encodeSAML2(samlRequest: string): string;
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import"../node-crypto-polyfill.js";import{DOMParser as b}from"@xmldom/xmldom";import{SignedXml as B}from"xml-crypto";import F from"xpath";import{deflateSync as H,inflateSync as J}from"fflate";import{createHash as q}from"crypto";import{ulid as W}from"ulid";import{AuthProviderType as u,DEFAULT_TEAM_CLAIM_NAME as K}from"@redocly/config";import{AUTH_URL as Y,JWT_SECRET_KEY as
|
|
1
|
+
import"../node-crypto-polyfill.js";import{DOMParser as b}from"@xmldom/xmldom";import{SignedXml as B}from"xml-crypto";import F from"xpath";import{deflateSync as H,inflateSync as J}from"fflate";import{createHash as q}from"crypto";import{ulid as W}from"ulid";import{AuthProviderType as u,DEFAULT_TEAM_CLAIM_NAME as K}from"@redocly/config";import{AUTH_URL as Y,JWT_SECRET_KEY as L}from"../constants/common.js";import{envConfig as Q}from"../config/env-config.js";import{getPathPrefix as X,withPathPrefix as G}from"@redocly/theme/core/utils";import{DEFAULT_AUTHENTICATED_TEAM as Z,REQUIRED_OIDC_SCOPES as R,ServerRoutes as N}from"../../constants/common.js";import{appendQueryParams as ee}from"../../utils/url/append-query-params.js";import{logger as te}from"../tools/notifiers/logger.js";import{randomString as ne}from"../utils/crypto/random-string.js";import{randomUUID as U}from"../utils/crypto/random-uuid.js";import{AlgorithmTypes as w,JwtTokenExpired as oe}from"./jwt/types.js";import*as f from"./jwt/jwt.js";import{parseTeamClaimToArray as re}from"../utils/index.js";import{arrayBufferToBase64 as ae,decodeBase64 as P,encodeBase64URL as se,urlSafeBase64 as v}from"./jwt/encode.js";import{formatSamlCertificate as ce}from"./utils/format-saml-certificate.js";function E(e){return e?.type===u.OIDC}function ie(e){return e?.type===u.SAML2}async function Je(e,t){if(E(t))return ue(e,t);if(ie(t))return de(e,t)}async function ue(e,t){const o=await V(e,t),n=new Set((t.scopes||[]).concat(R)),r=t.authorizationRequestCustomParams||{};return{type:u.OIDC,idpId:e,name:"OAuth provider",authorizationEndpoint:o.authorization_endpoint,clientId:t.clientId,responseType:"code",scope:Array.from(n).join(" "),extraParams:r,pkce:t.pkce}}function de(e,t){return{type:u.SAML2,idpId:e,name:"SAML2 provider",ssoUrl:t.ssoUrl,issuerId:t.issuerId,entityId:t.entityId||t.issuerId}}async function qe(e,t,o,n,r={}){const a=new Set((n.scopes||[]).concat(R));return await fetch(e,{method:"POST",body:new URLSearchParams({client_id:n.clientId,scope:Array.from(a).join(" "),code:t,redirect_uri:j(o),grant_type:"authorization_code",...n.clientSecret?{client_secret:n.clientSecret}:{},...r}).toString(),headers:{"Content-Type":"application/x-www-form-urlencoded",Accept:"application/json"}}).then(s=>s.json())}function le(e,{authorizationEndpoint:t,clientId:o,responseType:n,scope:r,extraParams:a,idpId:s,pkce:d},m,A,p){if(!t||!o||!n||!r)return{loginUrl:void 0};const c=new URL(t),h=p?.redirectUriOverride??`${e}${G(N.OIDC_CALLBACK)}`,_={state:U(),idpId:s,redirectUri:h,redirectTo:m,branch:p?.branchOverride??me(e),inviteCode:A,source:p?.sourceOverride??"portal",uiLocales:p?.uiLocales},y={};if(d){const l=v(ne(50)),g=v(q("sha256").update(l).digest("base64")),x="S256";c.searchParams.append("code_challenge",g),c.searchParams.append("code_challenge_method",x),y.code_verifier={value:l,options:{secure:!0,httpOnly:!0,expires:new Date(Date.now()+1e3*60*10),path:X()||"/"}}}c.searchParams.append("client_id",o),c.searchParams.append("scope",r),c.searchParams.append("response_type",n),c.searchParams.append("redirect_uri",j(h)),c.searchParams.append("state",se(JSON.stringify(_))),p?.uiLocales&&c.searchParams.append("ui_locales",p.uiLocales);for(const l in a)a[l]!==void 0&&c.searchParams.append(l,a[l]);return{loginUrl:c.toString(),cookies:y}}function We(e,t,o,n){const r=new URL(e);return r.searchParams.append("post_logout_redirect_uri",t),n&&r.searchParams.append("state",n),r.searchParams.append("id_token_hint",o),r.toString()}async function Ke(e){const t=Math.floor(Date.now()/1e3),o=t+(e.ttlSec??600);return f.sign({type:"mcp_auth_code",client_id:e.clientId,redirect_uri:e.redirectUri,id_token:e.idToken,...e.idpAccessToken?{idp_access_token:e.idpAccessToken}:{},...e.codeChallenge?{code_challenge:e.codeChallenge}:{},...e.codeChallengeMethod?{code_challenge_method:e.codeChallengeMethod}:{},iat:t,exp:o},L,w.HS256)}async function Ye(e){await f.verify(e,L,w.HS256);const{payload:t}=f.decode(e);if(t.type!=="mcp_auth_code")throw new Error("Invalid authorization code type");if(!t.client_id||!t.redirect_uri)throw new Error("Authorization code missing required claims");if(typeof t.exp=="number"&&Date.now()>=t.exp*1e3)throw new Error("Authorization code expired");return t}function Qe(e){const t=e||W(),o=t.startsWith("mcp_")?t:`mcp_${t}`;return{id:o,object:"mcp_session",uri:`urn:redocly:realm:mcp:session:${o}`}}function j(e){return e.match(/^https:\/\/preview-[^\.]+--/)?"https://previewauth--"+e.split("--")[1]:e.match(/^(https:\/\/[^\.]+)--[^\.]+\.preview\./)?e.replace(/^(https:\/\/[^\.]+?)--[^\.]+\.preview\./,"$1.previewauth."):e}function me(e){return e.match(/^(https:\/\/[^\.]+)--([^\.]+)\.preview\./)?.[2]||void 0}function pe(e){return e.type===u.OIDC}function fe(e){return e.type===u.SAML2}function Xe(e,t,o,n,r){return pe(e)?le(t,e,o,n,{uiLocales:r}):fe(e)?he(t,e,o,n,r):{}}function he(e,t,o,n,r){const s=`<samlp:AuthnRequest xmlns:samlp="urn:oasis:names:tc:SAML:2.0:protocol"
|
|
2
2
|
xmlns:saml="urn:oasis:names:tc:SAML:2.0:assertion"
|
|
3
3
|
Version="2.0"
|
|
4
4
|
ID="_${U()}"
|
|
@@ -9,4 +9,4 @@ import"../node-crypto-polyfill.js";import{DOMParser as b}from"@xmldom/xmldom";im
|
|
|
9
9
|
<samlp:NameIDPolicy
|
|
10
10
|
AllowCreate="true"
|
|
11
11
|
Format="urn:oasis:names:tc:SAML:2.0:nameid-format:persistent"/>
|
|
12
|
-
</samlp:AuthnRequest>`,
|
|
12
|
+
</samlp:AuthnRequest>`,d=ye(s);return{loginUrl:ee(t.ssoUrl,{SAMLRequest:d,RelayState:JSON.stringify({idpId:t.idpId,redirectTo:o,inviteCode:n,source:"portal",uiLocales:r})})}}function ye(e){return ae(H(new TextEncoder().encode(e)).buffer)}function Ge(e){const t=P(e);if(t.startsWith("<samlp:Response")||t.indexOf("<saml2p:Response")>-1)return t;const o=J(new Uint8Array(atob(e).split("").map(n=>n.charCodeAt(0))));return new TextDecoder().decode(o)}function Ze(e){try{return JSON.parse(P(e||""))}catch{throw new Error("Invalid OAuth2 state")}}function et(e){const t=new b().parseFromString(e,"application/xml"),n=i(t,"//*[local-name(.)='StatusCode']/@Value")[0]?.nodeValue?.endsWith("Success")||!1,a=i(t,"//*[local-name(.)='Response']/@Destination")[0]?.nodeValue||"",s=i(t,"//*[local-name(.)='Assertion']//*[local-name(.)='Issuer']/text()")[0],d=s&&s.nodeValue||void 0,m=i(t,"//*[local-name(.)='Audience']/text()")[0],A=m&&m.nodeValue||void 0,c=i(t,"//*[local-name(.)='Assertion']//*[local-name(.)='X509Certificate']/text()")[0]?.nodeValue||"",h=i(t,"//*[local-name(.)='Subject']//*[local-name(.)='NameID']/text()")[0],_=h&&h.nodeValue||"",y=i(t,"//*[local-name(.)='Subject']//*[local-name(.)='NameID']/@Format")[0],l=y&&y.nodeValue||"",g=i(t,"//*[local-name(.)='Conditions']/@NotOnOrAfter")[0],x=we(g),M={},k=i(t,"//*[local-name(.)='AttributeStatement']//*[local-name(.)='Attribute']");if(k.length)for(const T of k){const D=i(T,"./@Name")[0];if(D.nodeValue){const O=i(T,"./*[local-name(.)='AttributeValue']/text()")[0];O?.nodeValue&&(M[D.nodeValue]=O.nodeValue)}}return{uid:_,success:n,expiresAt:x,issuerId:d,entityId:A,attrs:M,cert:c,nameFormat:l,destination:a}}function we(e){const t=typeof e?.nodeValue=="string"&&I(Date.parse(e.nodeValue)),o=I(Date.now()),n=I(Date.now()+720*60*1e3);return t?t>o&&t<n?n:t:o}function I(e){return Math.floor(e/1e3)}const C={},S={jwks:{}};async function V(e,t){if(!C[e]){const o=t.configurationUrl?await $(t.configurationUrl):t.configuration;C[e]=Se()?Ae(o):o}return C[e]}function Se(){const e=Q.REDOCLY_ENFORCE_RESIDENCY;return!!e&&e.includes("host.docker.internal")}function Ae(e){if(typeof e!="object"||e===null)return e;const t={...e};for(const o of Object.keys(t)){const n=t[o];typeof n=="string"&&n.includes("://localhost")&&(t[o]=n.replace("://localhost","://host.docker.internal"))}return t}async function _e(e){for(const t of Object.keys(e)){const o=e[t];if(!E(o))continue;const n=await V(t,o);if(n.jwks_uri){const r=await $(n.jwks_uri);for(const a of r.keys)S.jwks[a.kid]={...a,idpId:t}}}}async function $(e){return fetch(e,{headers:{Accept:"application/json"}}).then(t=>t.json())}async function tt(e){return fetch(`${Y}/oidc/userinfo`,{headers:{Accept:"application/json",Authorization:`Bearer ${e}`}}).then(t=>t.status===200?t.json():void 0).catch(()=>{})}function nt(e){if(!e.configurationUrl)return!1;const t=new URL(e.configurationUrl);return["localhost","127.0.0.1","blueharvest.cloud","bhstage.cloud","cloud.redocly.com","beta.redocly.com","cloud.eu.redocly.com","beta.eu.redocly.com","cba.au.redocly.com"].some(n=>ge(t.hostname,n))}function ge(e,t){return e===t||e.endsWith(`.${t}`)}async function ot(e,t){const o=new b().parseFromString(e,"application/xml"),n=i(o,"//*[local-name(.)='Signature' and namespace-uri(.)='http://www.w3.org/2000/09/xmldsig#']")[0];if(!n)throw new Error("Cannot find Signature in the SAML response");const r=ce(t),a=new B({publicCert:r});a.loadSignature(n);try{return a.checkSignature(e)}catch{return!1}}function rt(e,t,o,n){t==="urn:oasis:names:tc:SAML:2.0:nameid-format:transient"&&(e=o["http://schemas.microsoft.com/identity/claims/objectidentifier"]);let r;(t==="urn:oasis:names:tc:SAML:2.0:nameid-format:email"||t==="urn:oasis:names:tc:SAML:1.1:nameid-format:emailAddress")&&(r=e),t==="urn:oasis:names:tc:SAML:2.0:nameid-format:persistent"&&e?.match(/.+@.+/)&&(r=e);const a=o["http://schemas.xmlsoap.org/ws/2005/05/identity/claims/name"],s=a?.match(/.+@.+/);return r=r||o["http://schemas.xmlsoap.org/ws/2005/05/identity/claims/emailaddress"]||(s?a:void 0),r=r?.toLowerCase(),{sub:e,given_name:o["http://schemas.xmlsoap.org/ws/2005/05/identity/claims/givenname"],family_name:o["http://schemas.xmlsoap.org/ws/2005/05/identity/claims/surname"],name:o["http://schemas.microsoft.com/identity/claims/displayname"]||a,email:r,email_verified:!0,teams:n?re(o[n]):[]}}function z(e,t={}){return e.map(o=>t[o]||o)}async function at(e,t){if(!t)return{};const o=t.authorization;if(!o)return{};try{const n=f.decode(o);if(n.header.alg===w.RS256){S.jwks[n.header.kid]===void 0&&await _e(e);const m=S.jwks[n.header.kid];if(!m)return S.jwks[n.header.kid]=null,{};await f.verify(o,m,w.RS256)}else await f.verify(o,L,w.HS256);const r=n.payload.idpId||S.jwks[n.header.kid]?.idpId,a=e[r]||{},s=Ie(a),d=Le(a);return{...n.payload,email:n.payload.email?.toLowerCase(),idpId:r,teams:Array.from(new Set([...z(n.payload.teams||[],d),..."defaultTeams"in a&&a.defaultTeams||[],...z("teamsClaimName"in a&&n.payload[s||""]||[],d),Z])),name:xe(n.payload),isAuthenticated:!0,idpAccessToken:n.payload.idp_access_token||t.idp_access_token,federatedAccessToken:t.federated_access_token,federatedIdToken:t.federated_id_token,authCookie:o}}catch(n){n instanceof oe||te.error("Malformed JWT token: %s",n.message)}return{}}function xe(e){return(e.firstName&&e.lastName?`${e.firstName} ${e.lastName}`:e.name||e.given_name||e.firstName||e.lastName)||e.email}function Le(e){switch(e.type){case u.SAML2:return e.teamsAttributeMap;case u.OIDC:return e.teamsClaimMap;default:return}}function Ie(e){switch(e.type){case u.SAML2:return e.teamsAttributeName;case u.OIDC:return e.teamsClaimName;default:return K}}function i(e,t){return F.select(t,e)||[]}export{Xe as buildLoginUrl,le as buildOidcLoginUrl,We as buildOidcLogoutUrl,he as buildSAML2LoginUrl,Ke as createMcpAuthorizationCode,Qe as createMcpSessionResource,Ge as decodeSamlResponse,ye as encodeSAML2,rt as extractUserClaims,Je as getAuthProviderLoginParams,ue as getOidcLoginParams,V as getOidcMetadata,tt as getRedoclyTokenPayload,de as getSaml2LoginParams,at as getUserParamsFromCookies,xe as getUsernameFromPayload,E as isOidcProviderConfig,nt as isRedoclySso,ie as isSaml2ProviderConfig,qe as oidcExchangeCodeForToken,S as oidcJwksCache,C as oidcMetadataCache,Ze as parseOidcState,me as parsePreviewBranch,et as parseSamlResponse,j as rewritePreviewAuthRedirectUri,Ye as verifyMcpAuthorizationCode,ot as verifySAMLResponse};
|
|
@@ -1 +1 @@
|
|
|
1
|
-
import{setCookie as R,deleteCookie as q}from"hono/cookie";import{AuthProviderType as V}from"@redocly/config";import{withPathPrefix as I,getPathPrefix as _}from"@redocly/theme/core/utils";import{compareURIs as X}from"../../../utils/url/compare-uris.js";import{ensureArray as b}from"../../../utils/array/ensure-array.js";import{ALTERNATIVE_AUD_CLAIM_NAME as E,JWT_SECRET_KEY as $,ORG_SLUG as W,ORG_ID as Y}from"../../constants/common.js";import{DEFAULT_COOKIE_EXPIRATION as F,ServerRoutes as S}from"../../../constants/common.js";import{sanitizeRedirectPathname as B}from"../../../utils/url/sanitize-redirect-pathname.js";import{telemetry as k}from"../../telemetry/index.js";import{envConfig as z}from"../../config/env-config.js";import{getAuthProviderLoginParams as Q,isOidcProviderConfig as U,isSaml2ProviderConfig as Z,oidcExchangeCodeForToken as x,buildLoginUrl as ee,decodeSamlResponse as re,extractUserClaims as oe,parseSamlResponse as ne,parseOidcState as te,verifySAMLResponse as ie,getUsernameFromPayload as se,buildOidcLogoutUrl as ae,getOidcMetadata as H,getRedoclyTokenPayload as de,isRedoclySso as ce,rewritePreviewAuthRedirectUri as le,parsePreviewBranch as j,buildOidcLoginUrl as ue,createMcpSessionResource as A}from"../auth.js";import*as D from"../jwt/jwt.js";import{AlgorithmTypes as P}from"../jwt/types.js";import{handleErrorPageRender as pe}from"../utils.js";import{encodeBase64URL as ge}from"../jwt/encode.js";async function De(i){if(z.isProductionEnv)return i.newResponse(null,404,{});const{password:e,...r}=await i.req.json(),a=await D.sign({...r,name:r.username||r.email||"Unknown"},$,P.HS256);return R(i,"authorization",a,{path:_()||"/",httpOnly:!0,secure:!0,sameSite:"none"}),i.newResponse(null,200,{})}function Pe(){return async i=>{const e=i.get("logger"),r=encodeURIComponent(i.req.query("message")||"");e.error(`Login error: ${r}`);const a=`${S.LOGIN}/?error=${encodeURIComponent(r)}`;return i.newResponse(null,301,{Location:a})}}function N(i){if(!i||!i.includes(S.MCP_CALLBACK))return null;try{const e=i.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 ve(i){return async e=>{const r=e.get("logger"),a=i.getConfig().ssoDirect,n=te(e.req.query("state")),m=n.idpId,t=n.source==="mcp"||n.redirectTo&&typeof n.redirectTo=="string"&&n.redirectTo.includes(S.MCP_CALLBACK),c=t?N(typeof n.redirectTo=="string"?n.redirectTo:void 0):null,s=a?.[m];if(!U(s))return r.error("OIDC login error: missing OIDC provider config"),e.text("Forbidden",403);const d=await H(m,s);if(a&&!d.token_endpoint){const u="Invalid OIDC configuration: token_endpoint is required";return r.error(`OIDC login error: ${u}`),e.text(u,500)}try{const u=d.token_endpoint,l=e.req.query("code"),h=e.req.query("error");if(h)return t&&k.sendMcpAuthorizationFailedMessage([{...A(c),error:`OIDC error: ${h}`,error_details:e.req.query("error_description")||null}]),pe(e,i,{slug:"/"},403,"403OIDC");if(!l){const w="Code is expected but not present";return r.error(`OIDC login error: ${w}`),t&&k.sendMcpAuthorizationFailedMessage([{...A(c),error:w,error_details:null}]),new Response(`Forbidden: ${w}`,{status:403})}const C=typeof n.redirectUri=="string"?n.redirectUri:new URL(I(S.OIDC_CALLBACK),e.req.url).toString(),p=e.get("cookies")?.code_verifier,g=await x(u,l,C,s,{...s.tokenRequestCustomParams,...p?{code_verifier:p}:{}});if(g.error)return r.error(`Error from OIDC provider: "${g.error}"`),t&&k.sendMcpAuthorizationFailedMessage([{...A(c),error:`Token exchange error: ${g.error}`,error_details:g.error_description||null}]),e.text(`Forbidden: ${g.error_description||g.error}`,403);if(!g?.id_token){const w="No id_token, please, add openid to scopes";return r.error(`OIDC login error: ${w}`),t&&k.sendMcpAuthorizationFailedMessage([{...A(c),error:w,error_details:null}]),new Response(`Forbidden: ${w}`,{status:403})}const{payload:f,header:L}=D.decode(g.id_token),o=L.alg===P.RS256;if(s.audience?.length&&![...b(f.aud||[]),...b(f[E]||[])].some(M=>s.audience?.includes(M))){const M="No valid audience found in id_token";return r.error(`OIDC login error: ${M}`),t&&k.sendMcpAuthorizationFailedMessage([{...A(c),error:M,error_details:null}]),new Response(`Forbidden: ${M}`)}const v=o?g.id_token:await D.sign({...f,idpId:m},$,P.HS256);se(f)||r.warn("To display your username, the required 'email' or 'full_profile' scope must be added to the identity provider configuration");const O=s?.tokenExpirationTime?Date.now()+s.tokenExpirationTime*1e3:f.exp*1e3||Date.now()+F*1e3;if(s.introspectEndpoint){const w=await fetch(s.introspectEndpoint,{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({access_token:g.access_token})});if(w.ok){const T=(await w.json()).ext?.federatedIdentity;T&&(R(e,"federated_access_token",T.access_token||"",{path:_()||"/",httpOnly:!1,expires:new Date(O)}),R(e,"federated_id_token",T.id_token||"",{path:_()||"/",httpOnly:!1,expires:new Date(O)}))}else r.warn(`OIDC introspect error: ${w.statusText}`)}if(R(e,"authorization",v,{path:_()||"/",httpOnly:!0,expires:new Date(O)}),v!==g.id_token&&R(e,"idp_id_token",g.id_token||"",{path:_()||"/",httpOnly:!0,expires:new Date(O)}),R(e,"idp_access_token",g.access_token||"",{path:_()||"/",httpOnly:!0,expires:new Date(O)}),q(e,"code_verifier",{path:_()||"/"}),t&&n.redirectTo&&typeof n.redirectTo=="string"&&n.redirectTo.includes(S.MCP_CALLBACK)){const M=`${e.req.url.split("?")[0].replace(S.OIDC_CALLBACK,"")}${n.redirectTo}`;return e.newResponse(null,302,{Location:M})}const K=typeof n.redirectTo=="string"?n.redirectTo:void 0;let J=B(new URL(K||"/",e.req.url).pathname);const G=e.newResponse(null,302,{Location:J});return r.updateContext({email:f.email,subject:f.sub}),r.info("OIDC login successful"),G}catch(u){const l=u instanceof Error?u.message:String(u),h=u instanceof Error?u.stack:String(u);if(r.error(`OIDC login error: ${l}`),t&&k.sendMcpAuthorizationFailedMessage([{...A(c),error:l,error_details:h}]),u.error==="access_denied")return r.info("Access denied"),e.text("Forbidden",403)}const y="Something went wrong";return r.error(`OIDC login error: ${y}`),t&&k.sendMcpAuthorizationFailedMessage([{...A(c),error:y,error_details:null}]),e.text(y,500)}}function $e(i){return async e=>{const r=e.get("logger"),n=e.get("auth").claims?.idpId,t=i.getConfig().ssoDirect?.[n];if(e.req.method==="POST")return U(t)||q(e,"authorization",{path:_()||"/"}),r.info("Logout successful"),e.newResponse(null,200,{});let c;if(U(t)){const s=(await H(n,t)).end_session_endpoint;if(s){const d=new URL(e.req.url),y=e.req.header("x-forwarded-proto")||d.protocol.slice(0,-1)||"https",u=e.req.header("x-forwarded-host")||d.host,l=`${y}://${u}`,h=j(l),C=h?ge(JSON.stringify({branch:j(l)})):void 0,p=h?`${le(l)}/_auth/logout`:`${l}${I(S.POST_LOGOUT)}`;c=ae(s,p,e.get("cookies")?.idp_id_token||e.get("cookies")?.authorization||"",C)}}return r.info("Logout successful"),q(e,"authorization",{path:_()||"/"}),e.newResponse(null,302,{Location:c||I("/")})}}function Ue(i){return async e=>{const r=i.getConfig().access?.logoutReturnUrl,a=r||I("/");return e.newResponse(null,302,{Location:a})}}function Te(i){return async e=>{const r=e.get("logger"),a=e.req.param("code"),n=z.BH_API_URL,m=(t,c,s)=>t&&c?`${t} ${c.charAt(0)}`:s;try{if(!n)throw new Error("BH_API_URL is not set");const t=i.getConfig().ssoDirect;if(!t||!Object.keys(t).length)return r.warn("Invite no sso configured to handle"),e.redirect(I("/"));const c=await fetch(`${n}/user-invites/public/${a}`);if(!c.ok)return c.status===404?(r.warn(`Invite ${a} not found redirect to homepage`),e.redirect(I("/"))):(r.error("Invite error",await c.text()),e.redirect(I("/")));const s=await c.json(),d=new URL(I("/invite"),e.req.url);return d.searchParams.set("code",a),d.searchParams.set("org",s.organization.name),d.searchParams.set("invitedBy",m(s.invitedBy.firstName,s.invitedBy.lastName,s.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 qe(i){return async e=>{const r=e.get("logger"),a=i.getConfig().ssoDirect,n=new URL(e.req.url),m=e.req.query("inviteCode"),t=e.req.header("x-forwarded-proto")||n.protocol.slice(0,-1)||"https",c=e.req.header("x-forwarded-host")||n.host,s=`${t}://${c}`;let d=n.searchParams.get("idpId");const y=n.searchParams.get("redirectTo"),u=Object.keys(a||{})[0];d=d||u;const l=n.searchParams.get("mcp_redirect_uri"),h=!!l;if(!a?.[d]){const o="Invalid idpId";if(r.error(`IdP login error: ${o}`),h){const v=N(y||void 0);k.sendMcpAuthorizationFailedMessage([{...A(v),error:o,error_details:null}])}return e.text(`Forbidden: ${o}`,403)}const p=d&&a?await Q(d,a[d]):void 0,g={};for(const o of Object.keys(p?.extraParams||{}))g[o]=n.searchParams.get(o)||p?.extraParams?.[o]||void 0;let f,L={};if(h&&l&&p&&p.type===V.OIDC){r.info(`Building MCP OAuth login URL with redirect_uri: ${l}`);const o=ue("",{...p,extraParams:g},y,m,{redirectUriOverride:l,sourceOverride:"mcp",branchOverride:void 0});f=o.loginUrl,L=o.cookies||{}}else if(p){const o=ee({...p,extraParams:g},s,y,m);f=o.loginUrl,L=o.cookies||{}}return Object.keys(L).forEach(o=>{R(e,o,L[o].value,L[o].options)}),r.info(`IdP login initiated for ID '${d}'`),e.newResponse(null,302,{Location:f||new URL(e.req.url).pathname})}}function be(i){return async e=>{const r=e.get("logger"),a=await e.req.formData(),n=a.get("SAMLResponse"),m=a.get("RelayState");if(typeof n!="string"||typeof m!="string"){const o="SAMLResponse is required";return r.error(`SAML2 login error: ${o}`),e.text(`Bad request: ${o}`,400)}const t=re(n),{success:c,uid:s,nameFormat:d,attrs:y,issuerId:u,expiresAt:l}=ne(t),{idpId:h,redirectTo:C}=JSON.parse(m);if(!c){const o="SAML2 assertion is not successful";return r.error(`SAML2 login error: ${o}`),e.text(`Permission denied: ${o}`,401)}if(!l||Math.ceil(Date.now()/1e3)>=l){const o="SAML2 Token Expired";return r.error(`SAML2 login error: ${o}`),e.text(o,401)}const p=i.getConfig().ssoDirect?.[h];if(!p||!Z(p)){const o="Cannot find valid IdP";return r.error(`SAML2 login error: ${o}`),e.text(`Permission denied: ${o}`,401)}if(!(p.issuerId&&u&&X(p.issuerId,u))){const o="IssuerID is misconfigured or untrusted assertions issuer received";return r.error(`SAML2 login error: ${o}`),e.text(`Permission denied: ${o}`,401)}if(!await ie(t,p.x509PublicCert)){const o="SAMLResponse signature invalid";return r.error(`SAML2 login error: ${o}`),e.text(o,401)}const f=oe(s,d,y,p.teamsAttributeName);if(!f.sub){const o="The provider did not return a valid user identity.";return r.error(`SAML2 login error: ${o}`),e.text(o,400)}if(!f.email){const o="The provider did not return a valid user email.";return r.error(`SAML2 login error: ${o}`),e.text(o,400)}const L=await D.sign({...f,idpId:h},$,P.HS256);return R(e,"authorization",L,{path:_()||"/",httpOnly:!0,expires:new Date(l*1e3)}),r.updateContext({email:f.email,subject:f.sub}),r.info("SAML2 login successful"),e.newResponse(null,302,{Location:C||"/"})}}function Ee(i){return async e=>{const r=e.get("logger"),a=new URL(e.req.query("redirectTo")||"/",e.req.url),n=I(B(a.pathname)),m=i.getConfig().ssoDirect,t=Object.entries(m||{}).find(([,C])=>U(C)&&ce(C));if(!(m&&t))return e.newResponse(null,302,{Location:n});const s=e.req.query("token"),d=s&&await de(s);if(!d)return e.newResponse(null,302,{Location:n});if(!b(d[E]||[]).some(C=>C===W||C===Y))return e.newResponse(null,302,{Location:n});const l=await D.sign({...d,idpId:t?.at(0)},$,P.HS256),h=Date.now()+F*1e3;return R(e,"authorization",l,{path:_()||"/",httpOnly:!0,expires:new Date(h),sameSite:"None",secure:!0}),r.info("Token login successful"),e.newResponse(null,302,{Location:n})}}export{De as authorizeHandler,qe as idpLoginHandler,Te as inviteHandler,$e as logoutHandler,ve as oidcCallbackHandler,Ue as postLogoutHandler,Pe as redoclyLoginCallbackHandler,Ee as redoclyTokenLoginHandler,be as samlCallbackHandler};
|
|
1
|
+
import{setCookie as R,deleteCookie as b}from"hono/cookie";import{AuthProviderType as X}from"@redocly/config";import{withPathPrefix as I,getPathPrefix as _}from"@redocly/theme/core/utils";import{compareURIs as W}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 U,ORG_SLUG as Y,ORG_ID as Q}from"../../constants/common.js";import{DEFAULT_COOKIE_EXPIRATION as B,ServerRoutes as O}from"../../../constants/common.js";import{sanitizeRedirectPathname as z}from"../../../utils/url/sanitize-redirect-pathname.js";import{telemetry as k}from"../../telemetry/index.js";import{envConfig as H}from"../../config/env-config.js";import{getAuthProviderLoginParams as Z,isOidcProviderConfig as $,isSaml2ProviderConfig as x,oidcExchangeCodeForToken as ee,buildLoginUrl as re,decodeSamlResponse as oe,extractUserClaims as ne,parseSamlResponse as te,parseOidcState as ie,verifySAMLResponse as se,getUsernameFromPayload as ae,buildOidcLogoutUrl as de,getOidcMetadata as j,getRedoclyTokenPayload as ce,isRedoclySso as le,rewritePreviewAuthRedirectUri as ue,parsePreviewBranch as N,buildOidcLoginUrl as pe,createMcpSessionResource as A}from"../auth.js";import*as D from"../jwt/jwt.js";import{AlgorithmTypes as v}from"../jwt/types.js";import{handleErrorPageRender as ge}from"../utils.js";import{encodeBase64URL as fe}from"../jwt/encode.js";import{resolveUiLocalesForIdpLogin as me}from"./helpers/resolve-ui-locales-for-idp-login.js";async function ve(i){if(H.isProductionEnv)return i.newResponse(null,404,{});const{password:e,...r}=await i.req.json(),a=await D.sign({...r,name:r.username||r.email||"Unknown"},U,v.HS256);return R(i,"authorization",a,{path:_()||"/",httpOnly:!0,secure:!0,sameSite:"none"}),i.newResponse(null,200,{})}function Ue(){return async i=>{const e=i.get("logger"),r=encodeURIComponent(i.req.query("message")||"");e.error(`Login error: ${r}`);const a=`${O.LOGIN}/?error=${encodeURIComponent(r)}`;return i.newResponse(null,301,{Location:a})}}function K(i){if(!i||!i.includes(O.MCP_CALLBACK))return null;try{const e=i.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 $e(i){return async e=>{const r=e.get("logger"),a=i.getConfig().ssoDirect,o=ie(e.req.query("state")),m=o.idpId,t=o.source==="mcp"||o.redirectTo&&typeof o.redirectTo=="string"&&o.redirectTo.includes(O.MCP_CALLBACK),c=t?K(typeof o.redirectTo=="string"?o.redirectTo:void 0):null,s=a?.[m];if(!$(s))return r.error("OIDC login error: missing OIDC provider config"),e.text("Forbidden",403);const d=await j(m,s);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&&k.sendMcpAuthorizationFailedMessage([{...A(c),error:`OIDC error: ${h}`,error_details:e.req.query("error_description")||null}]),ge(e,i,{slug:"/"},403,"403OIDC");if(!u){const y="Code is expected but not present";return r.error(`OIDC login error: ${y}`),t&&k.sendMcpAuthorizationFailedMessage([{...A(c),error:y,error_details:null}]),new Response(`Forbidden: ${y}`,{status:403})}const C=typeof o.redirectUri=="string"?o.redirectUri:new URL(I(O.OIDC_CALLBACK),e.req.url).toString(),w=e.get("cookies")?.code_verifier,l=await ee(p,u,C,s,{...s.tokenRequestCustomParams,...w?{code_verifier:w}:{}});if(l.error)return r.error(`Error from OIDC provider: "${l.error}"`),t&&k.sendMcpAuthorizationFailedMessage([{...A(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 y="No id_token, please, add openid to scopes";return r.error(`OIDC login error: ${y}`),t&&k.sendMcpAuthorizationFailedMessage([{...A(c),error:y,error_details:null}]),new Response(`Forbidden: ${y}`,{status:403})}const{payload:f,header:S}=D.decode(l.id_token),n=S.alg===v.RS256;if(s.audience?.length&&![...q(f.aud||[]),...q(f[F]||[])].some(M=>s.audience?.includes(M))){const M="No valid audience found in id_token";return r.error(`OIDC login error: ${M}`),t&&k.sendMcpAuthorizationFailedMessage([{...A(c),error:M,error_details:null}]),new Response(`Forbidden: ${M}`)}const g=n?l.id_token:await D.sign({...f,idpId:m},U,v.HS256);ae(f)||r.warn("To display your username, the required 'email' or 'full_profile' scope must be added to the identity provider configuration");const P=s?.tokenExpirationTime?Date.now()+s.tokenExpirationTime*1e3:f.exp*1e3||Date.now()+B*1e3;if(s.introspectEndpoint){const y=await fetch(s.introspectEndpoint,{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({access_token:l.access_token})});if(y.ok){const T=(await y.json()).ext?.federatedIdentity;T&&(R(e,"federated_access_token",T.access_token||"",{path:_()||"/",httpOnly:!1,expires:new Date(P)}),R(e,"federated_id_token",T.id_token||"",{path:_()||"/",httpOnly:!1,expires:new Date(P)}))}else r.warn(`OIDC introspect error: ${y.statusText}`)}if(R(e,"authorization",g,{path:_()||"/",httpOnly:!0,expires:new Date(P)}),g!==l.id_token&&R(e,"idp_id_token",l.id_token||"",{path:_()||"/",httpOnly:!0,expires:new Date(P)}),R(e,"idp_access_token",l.access_token||"",{path:_()||"/",httpOnly:!0,expires:new Date(P)}),b(e,"code_verifier",{path:_()||"/"}),t&&o.redirectTo&&typeof o.redirectTo=="string"&&o.redirectTo.includes(O.MCP_CALLBACK)){const M=`${e.req.url.split("?")[0].replace(O.OIDC_CALLBACK,"")}${o.redirectTo}`;return e.newResponse(null,302,{Location:M})}const G=typeof o.redirectTo=="string"?o.redirectTo:void 0;let J=z(new URL(G||"/",e.req.url).pathname);const V=e.newResponse(null,302,{Location:J});return r.updateContext({email:f.email,subject:f.sub}),r.info("OIDC login successful"),V}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&&k.sendMcpAuthorizationFailedMessage([{...A(c),error:u,error_details:h}]),p.error==="access_denied")return r.info("Access denied"),e.text("Forbidden",403)}const L="Something went wrong";return r.error(`OIDC login error: ${L}`),t&&k.sendMcpAuthorizationFailedMessage([{...A(c),error:L,error_details:null}]),e.text(L,500)}}function Te(i){return async e=>{const r=e.get("logger"),o=e.get("auth").claims?.idpId,t=i.getConfig().ssoDirect?.[o];if(e.req.method==="POST")return $(t)||b(e,"authorization",{path:_()||"/"}),r.info("Logout successful"),e.newResponse(null,200,{});let c;if($(t)){const s=(await j(o,t)).end_session_endpoint;if(s){const d=new URL(e.req.url),L=e.req.header("x-forwarded-proto")||d.protocol.slice(0,-1)||"https",p=e.req.header("x-forwarded-host")||d.host,u=`${L}://${p}`,h=N(u),C=h?fe(JSON.stringify({branch:N(u)})):void 0,w=h?`${ue(u)}/_auth/logout`:`${u}${I(O.POST_LOGOUT)}`;c=de(s,w,e.get("cookies")?.idp_id_token||e.get("cookies")?.authorization||"",C)}}return r.info("Logout successful"),b(e,"authorization",{path:_()||"/"}),e.newResponse(null,302,{Location:c||I("/")})}}function be(i){return async e=>{const r=i.getConfig().access?.logoutReturnUrl,a=r||I("/");return e.newResponse(null,302,{Location:a})}}function qe(i){return async e=>{const r=e.get("logger"),a=e.req.param("code"),o=H.BH_API_URL,m=(t,c,s)=>t&&c?`${t} ${c.charAt(0)}`:s;try{if(!o)throw new Error("BH_API_URL is not set");const t=i.getConfig().ssoDirect;if(!t||!Object.keys(t).length)return r.warn("Invite no sso configured to handle"),e.redirect(I("/"));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(I("/"))):(r.error("Invite error",await c.text()),e.redirect(I("/")));const s=await c.json(),d=new URL(I("/invite"),e.req.url);return d.searchParams.set("code",a),d.searchParams.set("org",s.organization.name),d.searchParams.set("invitedBy",m(s.invitedBy.firstName,s.invitedBy.lastName,s.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 Ee(i){return async e=>{const r=e.get("logger"),a=i.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,s=`${t}://${c}`;let d=o.searchParams.get("idpId");const L=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 E=K(L||void 0);k.sendMcpAuthorizationFailedMessage([{...A(E),error:g,error_details:null}])}return e.text(`Forbidden: ${g}`,403)}const w=me({localePrefixParam:o.searchParams.get("localePrefix"),l10n:i.getGlobalData()?.l10n}),l=d&&a?await Z(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;let S,n={};if(h&&u&&l&&l.type===X.OIDC){r.info(`Building MCP OAuth login URL with redirect_uri: ${u}`);const g=pe("",{...l,extraParams:f},L,m,{redirectUriOverride:u,sourceOverride:"mcp",branchOverride:void 0,uiLocales:w});S=g.loginUrl,n=g.cookies||{}}else if(l){const g=re({...l,extraParams:f},s,L,m,w);S=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:S||new URL(e.req.url).pathname})}}function Fe(i){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=oe(o),{success:c,uid:s,nameFormat:d,attrs:L,issuerId:p,expiresAt:u}=te(t),{idpId:h,redirectTo:C}=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 w=i.getConfig().ssoDirect?.[h];if(!w||!x(w)){const n="Cannot find valid IdP";return r.error(`SAML2 login error: ${n}`),e.text(`Permission denied: ${n}`,401)}if(!(w.issuerId&&p&&W(w.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 se(t,w.x509PublicCert)){const n="SAMLResponse signature invalid";return r.error(`SAML2 login error: ${n}`),e.text(n,401)}const f=ne(s,d,L,w.teamsAttributeName);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 S=await D.sign({...f,idpId:h},U,v.HS256);return R(e,"authorization",S,{path:_()||"/",httpOnly:!0,expires:new Date(u*1e3)}),r.updateContext({email:f.email,subject:f.sub}),r.info("SAML2 login successful"),e.newResponse(null,302,{Location:C||"/"})}}function Be(i){return async e=>{const r=e.get("logger"),a=new URL(e.req.query("redirectTo")||"/",e.req.url),o=I(z(a.pathname)),m=i.getConfig().ssoDirect,t=Object.entries(m||{}).find(([,C])=>$(C)&&le(C));if(!(m&&t))return e.newResponse(null,302,{Location:o});const s=e.req.query("token"),d=s&&await ce(s);if(!d)return e.newResponse(null,302,{Location:o});if(!q(d[F]||[]).some(C=>C===Y||C===Q))return e.newResponse(null,302,{Location:o});const u=await D.sign({...d,idpId:t?.at(0)},U,v.HS256),h=Date.now()+B*1e3;return R(e,"authorization",u,{path:_()||"/",httpOnly:!0,expires:new Date(h),sameSite:"None",secure:!0}),r.info("Token login successful"),e.newResponse(null,302,{Location:o})}}export{ve as authorizeHandler,Ee as idpLoginHandler,qe as inviteHandler,Te as logoutHandler,$e as oidcCallbackHandler,be as postLogoutHandler,Ue as redoclyLoginCallbackHandler,Be as redoclyTokenLoginHandler,Fe as samlCallbackHandler};
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
export type IdpLoginL10n = {
|
|
2
|
+
defaultLocale: string;
|
|
3
|
+
locales: {
|
|
4
|
+
code: string;
|
|
5
|
+
}[];
|
|
6
|
+
};
|
|
7
|
+
export declare function resolveUiLocalesForIdpLogin(options: {
|
|
8
|
+
localePrefixParam: string | null;
|
|
9
|
+
l10n: IdpLoginL10n | undefined;
|
|
10
|
+
}): string | undefined;
|
|
11
|
+
//# sourceMappingURL=resolve-ui-locales-for-idp-login.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
function i(o,n){if(!o?.trim()||!n?.length)return;const e=o.split("/").filter(Boolean)[0];return e?n.find(t=>t.code.toLowerCase()===e.toLowerCase())?.code:void 0}function a(o){const{localePrefixParam:n,l10n:e}=o;return(e?i(n,e.locales):void 0)??e?.defaultLocale}export{a as resolveUiLocalesForIdpLogin};
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
export type ClientMetadataDocument = {
|
|
2
|
+
redirect_uris: string[];
|
|
3
|
+
client_name?: string;
|
|
4
|
+
grant_types?: string[];
|
|
5
|
+
response_types?: string[];
|
|
6
|
+
scope?: string;
|
|
7
|
+
token_endpoint_auth_method?: string;
|
|
8
|
+
};
|
|
9
|
+
export declare function isRedirectUriRegistered(redirectUri: string, registeredUris: string[], options?: {
|
|
10
|
+
ignorePort?: boolean;
|
|
11
|
+
}): boolean;
|
|
12
|
+
export declare function isClientMetadataDocumentUrl(clientId: string | null | undefined): clientId is string;
|
|
13
|
+
export declare function fetchClientMetadataDocument(clientId: string): Promise<ClientMetadataDocument>;
|
|
14
|
+
//# sourceMappingURL=mcp-client-metadata.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
import l from"@redocly/ajv";import{readStreamWithSizeLimit as p}from"../../../api-routes/helpers/read-stream-with-size-limit.js";import{ResponseSizeLimitError as m}from"../../../api-routes/errors/response-size-limit.js";const d=new l({allErrors:!0,strictSchema:!1,strictTypes:!1}),u={type:"object",properties:{redirect_uris:{type:"array",items:{type:"string",minLength:1},minItems:1},client_name:{type:"string",nullable:!0},grant_types:{type:"array",items:{type:"string"},nullable:!0},response_types:{type:"array",items:{type:"string"},nullable:!0},scope:{type:"string",nullable:!0},token_endpoint_auth_method:{type:"string",nullable:!0}},required:["redirect_uris"],additionalProperties:!0},i=d.compile(u),f=5e3,s=.0625;function S(e,t,r){if(!r?.ignorePort)return t.includes(e);const n=c(e);return n?t.some(o=>{const a=c(o);return!!a&&a.protocol===n.protocol&&a.hostname===n.hostname&&a.pathname===n.pathname}):!1}function c(e){try{return new URL(e)}catch{return null}}function h(e){if(!e)return!1;let t;try{t=new URL(e)}catch{return!1}return t.protocol==="https:"}async function _(e){if(!h(e))throw new Error("client_id must be an https URL");const t=await fetch(e,{method:"GET",redirect:"error",headers:{Accept:"application/json"},signal:AbortSignal.timeout(f)});if(!t.ok)throw new Error(`Failed to fetch client metadata: HTTP ${t.status}`);const r=t.headers.get("content-type")||"";if(!/^application\/(.+\+)?json/i.test(r))throw new Error(`Unexpected content-type for client metadata: ${r}`);if(!t.body)throw new Error("Client metadata response has no body");const n=await p({stream:t.body,maxSizeMB:s,onSizeExceededErrorHandler:()=>{throw new m(`Client metadata exceeds maximum size of ${s*1024} KB`)}});let o;try{o=JSON.parse(n.toString("utf-8"))}catch(a){throw new Error(`Invalid JSON in client metadata: ${a.message}`)}return y(o)}function y(e){if(!i(e)){const t=i.errors?.[0],r=t?`${t.instancePath||"/"} ${t.message}`.trim():"Validation failed";throw new Error(`Invalid client metadata: ${r}`)}return e}export{_ as fetchClientMetadataDocument,h as isClientMetadataDocumentUrl,S as isRedirectUriRegistered};
|
|
@@ -5,8 +5,11 @@ export type McpContextPayload = {
|
|
|
5
5
|
mcpClientId: string | null;
|
|
6
6
|
mcpState: string | null;
|
|
7
7
|
mcpSessionId: string;
|
|
8
|
+
mcpCodeChallenge?: string | null;
|
|
9
|
+
mcpCodeChallengeMethod?: string | null;
|
|
8
10
|
timestamp: number;
|
|
9
11
|
};
|
|
12
|
+
export declare function verifyPkce(codeChallenge: string, codeChallengeMethod: string | undefined, codeVerifier: string): boolean;
|
|
10
13
|
export declare function createMcpContextToken(context: McpContextPayload): Promise<string>;
|
|
11
14
|
export declare function verifyAndParseMcpContextToken(token: string): Promise<McpContextPayload>;
|
|
12
15
|
export declare function mcpOAuthProtectedResourceHandler(): Handler;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
import{getCookie as
|
|
1
|
+
import{getCookie as C}from"hono/cookie";import{createHash as P,timingSafeEqual as v}from"node:crypto";import{ulid as E}from"ulid";import{AUTH_URL as f,JWT_SECRET_KEY as M}from"../../../constants/common.js";import{ServerRoutes as _}from"../../../../constants/common.js";import{withPathPrefix as u}from"@redocly/theme/core/utils";import{telemetry as m}from"../../../telemetry/index.js";import{envConfig as T}from"../../../config/env-config.js";import{createMcpAuthorizationCode as R,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 k}from"../../utils/get-request-origin.js";import{fetchClientMetadataDocument as O,isClientMetadataDocumentUrl as y,isRedirectUriRegistered as L}from"./mcp-client-metadata.js";const o=(e,r,n=200,a)=>e.json(r,n,{"Content-Type":"application/json",...a??{}}),A=new Set(["S256"]),q="https://claude.ai/oauth/claude-code-client-metadata";function z(e,r,n){if(!A.has(r||""))return!1;const a=P("sha256").update(n).digest("base64url"),t=Buffer.from(a),s=Buffer.from(e);return t.length!==s.length?!1:v(t,s)}async function H(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 $(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 Q(){return async e=>{if(e.req.method!=="GET")return o(e,{error:"Method not allowed"},405,{Allow:"GET"});const r=k(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 X(){return async e=>{const r=k(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 x(){return async e=>{if(e.req.method!=="POST")return o(e,{error:"Method not allowed"},405);try{return o(e,{client_id:T.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 ee(){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=E();if(s&&!A.has(i||""))return o(e,{error:"invalid_request",error_description:`Unsupported code_challenge_method. Supported: ${[...A].join(", ")}`},400);if(y(t)&&!s)return o(e,{error:"invalid_request",error_description:"code_challenge is required for client_id_metadata_document clients"},400);if(y(t)&&a)try{const c=await O(t),l=t===q;if(!L(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=k(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 H(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),U=c instanceof Error?c.stack:String(c);m.sendMcpAuthorizationFailedMessage([{...h(d),error:l,error_details:U}]);const I=new URL(u(`${f}/oauth2/auth`));return I.search=n.toString(),e.redirect(I.toString())}}}function re(){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(T.OAUTH_CLIENT_ID&&i.client_id&&!y(i.client_id)&&i.client_id!==T.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 te(){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);if(a=t.mcpSessionId||null,!t.isMcpFlow||!t.originalRedirectUri)throw new Error("Invalid MCP context");const s=C(e,"idp_id_token")||C(e,"authorization"),i=C(e,"idp_access_token");if(!s)return m.sendMcpAuthorizationFailedMessage([{...h(a),error:"Authentication required",error_details:null}]),e.text("Authentication required",401);const d=await R({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{H as createMcpContextToken,ee as mcpAuthorizationHandler,te as mcpCallbackHandler,x as mcpDynamicClientRegistrationHandler,X as mcpOAuthAuthorizationServerHandler,Q as mcpOAuthProtectedResourceHandler,re as mcpTokenPortalHandler,$ as verifyAndParseMcpContextToken,z as verifyPkce};
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@redocly/redoc-revel",
|
|
3
|
-
"version": "0.133.0-next.
|
|
3
|
+
"version": "0.133.0-next.5",
|
|
4
4
|
"description": "",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|
|
@@ -76,7 +76,7 @@
|
|
|
76
76
|
"reactjs-popup": "2.0.6",
|
|
77
77
|
"semver": "7.7.3",
|
|
78
78
|
"shiki": "3.21.0",
|
|
79
|
-
"simple-git": "3.
|
|
79
|
+
"simple-git": "3.36.0",
|
|
80
80
|
"sitemap": "7.1.1",
|
|
81
81
|
"stream-http": "3.2.0",
|
|
82
82
|
"styled-components": "5.3.11",
|
|
@@ -90,14 +90,14 @@
|
|
|
90
90
|
"xpath": "0.0.34",
|
|
91
91
|
"yaml-ast-parser": "0.0.43",
|
|
92
92
|
"zod": "^3.25.76",
|
|
93
|
-
"@redocly/asyncapi-docs": "1.10.0-next.
|
|
93
|
+
"@redocly/asyncapi-docs": "1.10.0-next.5",
|
|
94
94
|
"@redocly/config": "0.48.1",
|
|
95
|
-
"@redocly/graphql-docs": "1.10.0-next.
|
|
96
|
-
"@redocly/openapi-docs": "3.21.0-next.
|
|
95
|
+
"@redocly/graphql-docs": "1.10.0-next.5",
|
|
96
|
+
"@redocly/openapi-docs": "3.21.0-next.5",
|
|
97
97
|
"@redocly/portal-legacy-ui": "0.16.0-next.0",
|
|
98
|
-
"@redocly/portal-plugin-mock-server": "0.18.0-next.
|
|
99
|
-
"@redocly/realm-asyncapi-sdk": "0.11.0-next.
|
|
100
|
-
"@redocly/theme": "0.65.0-next.
|
|
98
|
+
"@redocly/portal-plugin-mock-server": "0.18.0-next.5",
|
|
99
|
+
"@redocly/realm-asyncapi-sdk": "0.11.0-next.3",
|
|
100
|
+
"@redocly/theme": "0.65.0-next.5"
|
|
101
101
|
},
|
|
102
102
|
"peerDependencies": {
|
|
103
103
|
"react": "^19.2.4",
|