@redocly/redoc 0.137.0-next.3 → 0.137.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.
Files changed (40) hide show
  1. package/CHANGELOG.md +64 -0
  2. package/dist/cli/stats/collectors/openapi/index.js +1 -1
  3. package/dist/client/app/markdoc/custom-components/openapi/json-schema.js +1 -1
  4. package/dist/client/app/markdoc/custom-components/openapi/openapi-code-sample.js +1 -1
  5. package/dist/client/app/markdoc/custom-components/openapi/openapi-example.js +1 -1
  6. package/dist/client/app/markdoc/custom-components/openapi/openapi-response-sample.js +1 -1
  7. package/dist/client/app/markdoc/custom-components/openapi/use-markdown-adapter.d.ts +45 -0
  8. package/dist/client/app/markdoc/custom-components/openapi/use-markdown-adapter.js +1 -0
  9. package/dist/client/utils/catalog/get-next-page-param.d.ts +1 -1
  10. package/dist/client/utils/catalog/get-next-page-param.js +1 -1
  11. package/dist/compiled/svgo/svgo-node.js +32 -32
  12. package/dist/server/config/env-config.d.ts +2 -0
  13. package/dist/server/config/env-schema.d.ts +5 -0
  14. package/dist/server/config/env-schemas/search.d.ts +3 -0
  15. package/dist/server/config/env-schemas/search.js +1 -1
  16. package/dist/server/constants/plugins/search.d.ts +0 -2
  17. package/dist/server/constants/plugins/search.js +1 -1
  18. package/dist/server/plugins/markdown/utils/sanitize-malformed-md-content.js +2 -2
  19. package/dist/server/plugins/mcp/codemode/tools/manage-credentials.js +2 -2
  20. package/dist/server/plugins/mcp/gateway-mcp/elicitation.d.ts +2 -1
  21. package/dist/server/plugins/mcp/gateway-mcp/elicitation.js +4 -4
  22. package/dist/server/plugins/mcp/gateway-mcp/secret-store.d.ts +8 -0
  23. package/dist/server/plugins/mcp/gateway-mcp/upstream-auth-scheme.d.ts +2 -0
  24. package/dist/server/plugins/mcp/gateway-mcp/upstream-auth-scheme.js +2 -2
  25. package/dist/server/plugins/mcp/handlers/mcp-credentials-handler.js +1 -144
  26. package/dist/server/plugins/mcp/handlers/mcp-credentials-page.d.ts +6 -0
  27. package/dist/server/plugins/mcp/handlers/mcp-credentials-page.js +217 -0
  28. package/dist/server/plugins/mcp/index.js +1 -1
  29. package/dist/server/plugins/mcp/servers/mcp-server.js +3 -3
  30. package/dist/server/plugins/mcp/utils/find-gated-tool-names.d.ts +13 -0
  31. package/dist/server/plugins/mcp/utils/find-gated-tool-names.js +1 -0
  32. package/dist/server/plugins/search/engines/typesense/index.js +1 -1
  33. package/dist/server/plugins/search/index.js +1 -1
  34. package/dist/server/plugins/search/utils.d.ts +2 -1
  35. package/dist/server/plugins/search/utils.js +2 -2
  36. package/dist/server/store.d.ts +1 -0
  37. package/dist/server/store.js +1 -1
  38. package/dist/server/types/plugins/common.d.ts +3 -0
  39. package/dist/server/web-server/routes/mcp-routes/mcp-server-card.js +1 -1
  40. package/package.json +8 -8
@@ -56,6 +56,7 @@ declare function loadEnvConfig(): {
56
56
  REDOCLY_MCP_ENFORCE_TOKEN_AUDIENCE?: boolean | undefined;
57
57
  SEARCH_DEV_REINIT?: string | undefined;
58
58
  SEARCH_DEV_DEBUG: boolean;
59
+ REDOCLY_SKIP_TYPESENSE_INDEXING: boolean;
59
60
  TYPESENSE_API_URL?: string | undefined;
60
61
  TYPESENSE_API_KEY?: string | undefined;
61
62
  MAIN_API_URL?: string | undefined;
@@ -137,6 +138,7 @@ export declare const envConfig: {
137
138
  REDOCLY_MCP_ENFORCE_TOKEN_AUDIENCE?: boolean | undefined;
138
139
  SEARCH_DEV_REINIT?: string | undefined;
139
140
  SEARCH_DEV_DEBUG: boolean;
141
+ REDOCLY_SKIP_TYPESENSE_INDEXING: boolean;
140
142
  TYPESENSE_API_URL?: string | undefined;
141
143
  TYPESENSE_API_KEY?: string | undefined;
142
144
  MAIN_API_URL?: string | undefined;
@@ -61,6 +61,7 @@ export declare const envSchema: z.ZodEffects<z.ZodObject<{
61
61
  } & {
62
62
  SEARCH_DEV_REINIT: z.ZodOptional<z.ZodString>;
63
63
  SEARCH_DEV_DEBUG: z.ZodEffects<z.ZodDefault<z.ZodEnum<["true", "false"]>>, boolean, "false" | "true" | undefined>;
64
+ REDOCLY_SKIP_TYPESENSE_INDEXING: z.ZodEffects<z.ZodDefault<z.ZodEnum<["true", "false"]>>, boolean, "false" | "true" | undefined>;
64
65
  TYPESENSE_API_URL: z.ZodOptional<z.ZodString>;
65
66
  TYPESENSE_API_KEY: z.ZodOptional<z.ZodString>;
66
67
  } & {
@@ -138,6 +139,7 @@ export declare const envSchema: z.ZodEffects<z.ZodObject<{
138
139
  REDOCLY_MCP_ENFORCE_TOKEN_AUDIENCE?: boolean | undefined;
139
140
  SEARCH_DEV_REINIT?: string | undefined;
140
141
  SEARCH_DEV_DEBUG: boolean;
142
+ REDOCLY_SKIP_TYPESENSE_INDEXING: boolean;
141
143
  TYPESENSE_API_URL?: string | undefined;
142
144
  TYPESENSE_API_KEY?: string | undefined;
143
145
  MAIN_API_URL?: string | undefined;
@@ -209,6 +211,7 @@ export declare const envSchema: z.ZodEffects<z.ZodObject<{
209
211
  REDOCLY_MCP_ENFORCE_TOKEN_AUDIENCE?: "false" | "true" | undefined;
210
212
  SEARCH_DEV_REINIT?: string | undefined;
211
213
  SEARCH_DEV_DEBUG?: "false" | "true" | undefined;
214
+ REDOCLY_SKIP_TYPESENSE_INDEXING?: "false" | "true" | undefined;
212
215
  TYPESENSE_API_URL?: string | undefined;
213
216
  TYPESENSE_API_KEY?: string | undefined;
214
217
  MAIN_API_URL?: string | undefined;
@@ -280,6 +283,7 @@ export declare const envSchema: z.ZodEffects<z.ZodObject<{
280
283
  REDOCLY_MCP_ENFORCE_TOKEN_AUDIENCE?: boolean | undefined;
281
284
  SEARCH_DEV_REINIT?: string | undefined;
282
285
  SEARCH_DEV_DEBUG: boolean;
286
+ REDOCLY_SKIP_TYPESENSE_INDEXING: boolean;
283
287
  TYPESENSE_API_URL?: string | undefined;
284
288
  TYPESENSE_API_KEY?: string | undefined;
285
289
  MAIN_API_URL?: string | undefined;
@@ -351,6 +355,7 @@ export declare const envSchema: z.ZodEffects<z.ZodObject<{
351
355
  REDOCLY_MCP_ENFORCE_TOKEN_AUDIENCE?: "false" | "true" | undefined;
352
356
  SEARCH_DEV_REINIT?: string | undefined;
353
357
  SEARCH_DEV_DEBUG?: "false" | "true" | undefined;
358
+ REDOCLY_SKIP_TYPESENSE_INDEXING?: "false" | "true" | undefined;
354
359
  TYPESENSE_API_URL?: string | undefined;
355
360
  TYPESENSE_API_KEY?: string | undefined;
356
361
  MAIN_API_URL?: string | undefined;
@@ -5,16 +5,19 @@ import { z } from 'zod';
5
5
  export declare const searchSchema: z.ZodObject<{
6
6
  SEARCH_DEV_REINIT: z.ZodOptional<z.ZodString>;
7
7
  SEARCH_DEV_DEBUG: z.ZodEffects<z.ZodDefault<z.ZodEnum<["true", "false"]>>, boolean, "false" | "true" | undefined>;
8
+ REDOCLY_SKIP_TYPESENSE_INDEXING: z.ZodEffects<z.ZodDefault<z.ZodEnum<["true", "false"]>>, boolean, "false" | "true" | undefined>;
8
9
  TYPESENSE_API_URL: z.ZodOptional<z.ZodString>;
9
10
  TYPESENSE_API_KEY: z.ZodOptional<z.ZodString>;
10
11
  }, "strip", z.ZodTypeAny, {
11
12
  SEARCH_DEV_REINIT?: string | undefined;
12
13
  SEARCH_DEV_DEBUG: boolean;
14
+ REDOCLY_SKIP_TYPESENSE_INDEXING: boolean;
13
15
  TYPESENSE_API_URL?: string | undefined;
14
16
  TYPESENSE_API_KEY?: string | undefined;
15
17
  }, {
16
18
  SEARCH_DEV_REINIT?: string | undefined;
17
19
  SEARCH_DEV_DEBUG?: "false" | "true" | undefined;
20
+ REDOCLY_SKIP_TYPESENSE_INDEXING?: "false" | "true" | undefined;
18
21
  TYPESENSE_API_URL?: string | undefined;
19
22
  TYPESENSE_API_KEY?: string | undefined;
20
23
  }>;
@@ -1 +1 @@
1
- import{z as o}from"zod";import{envBooleanStringDefaultFalse as t}from"../../utils/envs/env-boolean-string.js";const n=o.object({SEARCH_DEV_REINIT:o.string().optional(),SEARCH_DEV_DEBUG:t(),TYPESENSE_API_URL:o.string().url().optional(),TYPESENSE_API_KEY:o.string().optional()});export{n as searchSchema};
1
+ import{z as E}from"zod";import{envBooleanStringDefaultFalse as o}from"../../utils/envs/env-boolean-string.js";const S=E.object({SEARCH_DEV_REINIT:E.string().optional(),SEARCH_DEV_DEBUG:o(),REDOCLY_SKIP_TYPESENSE_INDEXING:o(),TYPESENSE_API_URL:E.string().url().optional(),TYPESENSE_API_KEY:E.string().optional()});export{S as searchSchema};
@@ -1,4 +1,3 @@
1
- import type { SearchDocument } from '../../types';
2
1
  export declare const SEARCH_INDEX_FIELDS: string[];
3
2
  export declare const SEARCH_GROUP_NAME_DOCUMENTATION = "Documentation";
4
3
  export declare const SEARCH_GROUP_NAME_API = "API Reference";
@@ -12,7 +11,6 @@ export declare const AI_INDEX_EXPORT_FOLDER = "ai-search";
12
11
  export declare const AI_SEARCH_CHUNK_SIZE = 50;
13
12
  export declare const AI_SEARCH_DOCUMENT_CHUNK_SIZE = 250;
14
13
  export declare const SEARCH_DOCUMENT_METADATA_KEY: unique symbol;
15
- export declare const BASE_SEARCH_DOCUMENT: SearchDocument;
16
14
  export declare const DEFAULT_AI_SEARCH_ENABLED = true;
17
15
  export declare const DISABLE_DEEP_LINK_IF_FIELDS_EXIST: string[];
18
16
  //# sourceMappingURL=search.d.ts.map
@@ -1 +1 @@
1
- import{DEFAULT_ANONYMOUS_VISITOR_TEAM as e,DEFAULT_AUTHENTICATED_TEAM as t}from"../../../constants/common.js";const _=["title","text","path","httpPath","parameters[]:name","parameters[]:description","parameters[]:place","parameters[]:path"],A="Documentation",o="API Reference",r="search",p=100,T=10,a=25,n=100,s=150,c="ai-search",I=50,S=250,C=Symbol("metadata"),R={id:"",url:"",title:"",text:"",security:[],parameters:[{place:"",mediaType:void 0,name:"",description:"",path:[],type:"object",required:!1,example:void 0,enum:void 0}],rbacTeams:[e,t]},D=!0,H=["title"];export{c as AI_INDEX_EXPORT_FOLDER,I as AI_SEARCH_CHUNK_SIZE,S as AI_SEARCH_DOCUMENT_CHUNK_SIZE,R as BASE_SEARCH_DOCUMENT,D as DEFAULT_AI_SEARCH_ENABLED,H as DISABLE_DEEP_LINK_IF_FIELDS_EXIST,s as HIGHLIGHTED_TEXT_MAX_LENGTH,r as SEARCH_DATA_EXPORT_FOLDER,C as SEARCH_DOCUMENT_METADATA_KEY,T as SEARCH_GROUP_LIMIT,o as SEARCH_GROUP_NAME_API,A as SEARCH_GROUP_NAME_DOCUMENTATION,_ as SEARCH_INDEX_FIELDS,p as SEARCH_LIMIT,a as SEARCH_MAX_FACET_VALUES,n as SEARCH_MAX_INMEMORY_DOCUMENTS_COUNT};
1
+ const t=["title","text","path","httpPath","parameters[]:name","parameters[]:description","parameters[]:place","parameters[]:path"],_="Documentation",E="API Reference",e="search",o=100,A=10,r=25,p=100,I=150,s="ai-search",c=50,R=250,S=Symbol("metadata"),a=!0,n=["title"];export{s as AI_INDEX_EXPORT_FOLDER,c as AI_SEARCH_CHUNK_SIZE,R as AI_SEARCH_DOCUMENT_CHUNK_SIZE,a as DEFAULT_AI_SEARCH_ENABLED,n as DISABLE_DEEP_LINK_IF_FIELDS_EXIST,I as HIGHLIGHTED_TEXT_MAX_LENGTH,e as SEARCH_DATA_EXPORT_FOLDER,S as SEARCH_DOCUMENT_METADATA_KEY,A as SEARCH_GROUP_LIMIT,E as SEARCH_GROUP_NAME_API,_ as SEARCH_GROUP_NAME_DOCUMENTATION,t as SEARCH_INDEX_FIELDS,o as SEARCH_LIMIT,r as SEARCH_MAX_FACET_VALUES,p as SEARCH_MAX_INMEMORY_DOCUMENTS_COUNT};
@@ -1,2 +1,2 @@
1
- function l(r){return r.replace(/\[([^\]]*<[^>]*>[^\]]*)\]\(([^)]+)\)/g,(s,e,t)=>`[${e.replace(/</g,"&lt;").replace(/>/g,"&gt;")}](${t})`).replace(/({%\s*(\w+)(?:\s+[^%]*)?%})(\s*)({%\s*\/\2\s*%})/g,(s,e,t,n,a)=>`${e}
2
- ${a}`)}export{l as sanitizeMalformedMdContent};
1
+ function c(n){return n.replace(/\[([^\]]*<[^>]*>[^\]]*)\]\(([^)]+)\)/g,(r,t,s)=>`[${t.replace(/(`+)[^`]+\1|[<>]/g,e=>e==="<"?"&lt;":e===">"?"&gt;":e)}](${s})`).replace(/({%\s*(\w+)(?:\s+[^%]*)?%})(\s*)({%\s*\/\2\s*%})/g,(r,t,s,l,e)=>`${t}
2
+ ${e}`)}export{c as sanitizeMalformedMdContent};
@@ -1,2 +1,2 @@
1
- import{MANAGE_API_CREDENTIALS_TOOL_NAME as y}from"../../constants.js";import{buildUpstreamCredentialFallback as b,buildUpstreamCredentialInputRequired as v,schemeLabelOf as w}from"../../gateway-mcp/elicitation.js";import{getCallableApis as u,isFetchCapable as P}from"../../gateway-mcp/fetch-capability.js";import{secretStore as S}from"../../gateway-mcp/secret-store.js";import{MISSING_SESSION_HANDLE_MESSAGE as I,resolveSecretSubject as k}from"../../gateway-mcp/subject.js";import{resolveUpstreamApiAuth as j}from"../../gateway-mcp/upstream-auth-scheme.js";function q(e){return!!e.elicitationEnabled&&P(e)}function H(e){const t=u(e).map(s=>s.relativePath);return{name:y,description:["Get a secure link where the user sets, replaces, or removes the credentials this server sends to a documented API.","Call it when the user wants to change or clear an API key, or when a tool result tells you the stored credentials were rejected.","When a tool result lists several kinds of credential for an operation, ask the user which one they hold and wait for the answer before calling this tool, then pass that name as scheme.","It returns a link for the user to open - never ask for the secret in the conversation and never pass it as an argument."].join(`
2
- `),annotations:{title:"Manage API credentials",readOnlyHint:!1,openWorldHint:!1},schema:{type:"object",required:["api"],additionalProperties:!1,properties:{api:{type:"string",description:`The API to manage credentials for.${t.length>0?` One of: ${t.join(", ")}.`:""}`,minLength:1,...t.length>0?{enum:t}:{}},scheme:{type:"string",description:"Name of the security scheme to manage the credential for, as named in the API description (a tool result that reports a rejected credential names it). Omit it when the API has a single scheme.",minLength:1},sessionHandle:{type:"string",description:"Handle returned by createAnonymousSession. Required while the caller is anonymous, so the credentials are stored against the same subject the API calls use.",minLength:1}}}}}function U(e,c){return async t=>{const s=k(e.user,typeof t.sessionHandle=="string"?t.sessionHandle:void 0);if(s.kind==="invalid")return i(s.message);if(s.kind==="anonymous")return i(I);const{subject:d}=s,h=typeof t.api=="string"?t.api:"",n=u(e).find(l=>l.relativePath===h||l.name===h);if(!n){const l=u(e).map(A=>A.relativePath).join(", ");return i(`Unknown api "${h}". Credentials can only be managed for the APIs this server can call: ${l||"none"}.`)}const a=await j(e,n.relativePath),r=typeof t.scheme=="string"?t.scheme:void 0,m=Object.keys(a.schemes);if(r&&!a.schemes[r])return i(`Unknown scheme "${r}" for api "${n.relativePath}". The security schemes this server can store credentials for are: ${m.join(", ")}.`);if(!r&&m.length>1)return i(`The "${n.name}" API has more than one security scheme; call this tool again with scheme set to one of: ${m.join(", ")}.`);const o=r??a.defaultScheme,p=w(a.schemes,o),f=await c.getKv(),g={subject:d,baseUrl:e.baseUrl??"",target:{api:n.relativePath,name:n.name,scheme:o,...p?{schemeLabel:p}:{}},auth:a.schemes[o],kv:f,hasExistingSecret:await S.hasSecret(f,d,n.relativePath,o),reason:{kind:"requested"}};return c.getClientCapabilities()?.elicitation?.url?await v(g):{content:[{type:"text",text:await b(g)}],isError:!1}}}function i(e){return{content:[{type:"text",text:e}],isError:!0}}export{q as isManageApiCredentialsToolAvailable,U as manageApiCredentials,H as manageApiCredentialsSchema};
1
+ import{MANAGE_API_CREDENTIALS_TOOL_NAME as v}from"../../constants.js";import{buildUpstreamCredentialFallback as y,buildUpstreamCredentialInputRequired as b,schemeLabelOf as P}from"../../gateway-mcp/elicitation.js";import{getCallableApis as m,isFetchCapable as S}from"../../gateway-mcp/fetch-capability.js";import{secretStore as I}from"../../gateway-mcp/secret-store.js";import{MISSING_SESSION_HANDLE_MESSAGE as w,resolveSecretSubject as k}from"../../gateway-mcp/subject.js";import{resolveUpstreamApiAuth as j}from"../../gateway-mcp/upstream-auth-scheme.js";function H(e){return!!e.elicitationEnabled&&S(e)}function U(e){const t=m(e).map(n=>n.relativePath);return{name:v,description:["Get a secure link where the user sets, replaces, or removes the credentials this server sends to a documented API.","Call it when the user wants to change or clear an API key, or when a tool result tells you the stored credentials were rejected.","The link covers every credential the API accepts, so scheme is optional - pass it only when a tool result names the scheme that was rejected.","It returns a link for the user to open - never ask for the secret in the conversation and never pass it as an argument."].join(`
2
+ `),annotations:{title:"Manage API credentials",readOnlyHint:!1,openWorldHint:!1},schema:{type:"object",required:["api"],additionalProperties:!1,properties:{api:{type:"string",description:`The API to manage credentials for.${t.length>0?` One of: ${t.join(", ")}.`:""}`,minLength:1,...t.length>0?{enum:t}:{}},scheme:{type:"string",description:"Name of the security scheme to manage the credential for, as named in the API description (a tool result that reports a rejected credential names it). Omit it to let the link cover every scheme the API accepts.",minLength:1},sessionHandle:{type:"string",description:"Handle returned by createAnonymousSession. Required while the caller is anonymous, so the credentials are stored against the same subject the API calls use.",minLength:1}}}}}function _(e,c){return async t=>{const n=k(e.user,typeof t.sessionHandle=="string"?t.sessionHandle:void 0);if(n.kind==="invalid")return l(n.message);if(n.kind==="anonymous")return l(w);const{subject:u}=n,h=typeof t.api=="string"?t.api:"",s=m(e).find(o=>o.relativePath===h||o.name===h);if(!s){const o=m(e).map(g=>g.relativePath).join(", ");return l(`Unknown api "${h}". Credentials can only be managed for the APIs this server can call: ${o||"none"}.`)}const r=await j(e,s.relativePath),a=typeof t.scheme=="string"?t.scheme:void 0,A=Object.keys(r.schemes);if(a&&!r.schemes[a])return l(`Unknown scheme "${a}" for api "${s.relativePath}". The security schemes this server can store credentials for are: ${A.join(", ")}.`);const i=a??r.defaultScheme,p=P(r.schemes,i),d=await c.getKv(),f={subject:u,baseUrl:e.baseUrl??"",target:{api:s.relativePath,name:s.name,scheme:i,...p?{schemeLabel:p}:{}},auth:r.schemes[i],apiAuth:r,kv:d,hasExistingSecret:await I.hasSecret(d,u,s.relativePath,i),reason:{kind:"requested"}};return c.getClientCapabilities()?.elicitation?.url?await b(f):{content:[{type:"text",text:await y(f)}],isError:!1}}}function l(e){return{content:[{type:"text",text:e}],isError:!0}}export{H as isManageApiCredentialsToolAvailable,_ as manageApiCredentials,U as manageApiCredentialsSchema};
@@ -1,6 +1,6 @@
1
1
  import type { InputRequiredResult } from '@modelcontextprotocol/server';
2
2
  import type { UpstreamAuthRequiredEvent } from './upstream-auth.js';
3
- import type { SchemeChoice, UpstreamAuthScheme } from './upstream-auth-scheme.js';
3
+ import type { SchemeChoice, UpstreamApiAuth, UpstreamAuthScheme } from './upstream-auth-scheme.js';
4
4
  import type { KvService } from '@redocly/config';
5
5
  export declare const CREDENTIALS_INPUT_REQUEST_KEY = "credentials";
6
6
  export type CredentialTarget = {
@@ -25,6 +25,7 @@ type CredentialElicitationRequest = {
25
25
  baseUrl: string;
26
26
  target: CredentialTarget;
27
27
  auth: UpstreamAuthScheme;
28
+ apiAuth?: UpstreamApiAuth;
28
29
  kv: KvService;
29
30
  hasExistingSecret: boolean;
30
31
  reason: CredentialElicitationReason;
@@ -1,4 +1,4 @@
1
- import{inputRequired as c,inputResponse as $}from"@modelcontextprotocol/server";import{MANAGE_API_CREDENTIALS_TOOL_NAME as h}from"../constants.js";import{secretStore as T,ELICITATION_TTL_MS as A}from"./secret-store.js";import{ANONYMOUS_SUBJECT_PREFIX as l}from"./subject.js";const d="credentials";function O(e){const t=$(e,d);return t.kind==="elicit"&&(t.action==="decline"||t.action==="cancel")}function _(e,t){return Object.keys(e).length>1?t:void 0}async function L(e){const{url:t}=await u(e);return c({inputRequests:{[d]:c.elicitUrl({message:y(e.target,e.hasExistingSecret),url:t})}})}async function N(e){const{url:t}=await u(e);return b(e,t,w(e.subject))}function S(e,t){const n=e.status===401?`The stored credentials for "${e.name}" were rejected (HTTP 401).`:`The "${e.name}" API rejected the request with HTTP ${e.status}.`,i=t?`api: "${e.api}" and scheme: "${t}"`:`api: "${e.api}"`;return[n,"",`Do not retry the same call. Call \`${h}\` with ${i} to get a link the user has to open in a browser to replace or remove the stored credentials, then retry once they confirm they saved them.`].join(`
2
- `)}function j(e,t){const n=t.map(({name:i,label:r,description:a})=>`- ${i} \u2014 ${r}${a?`: ${a}`:""}`);return[`The "${e.name}" API rejected the request with HTTP ${e.status} and this operation accepts more than one kind of credential:`,"",...n,"","Ask the user which one of these they have, and wait for the answer. Never guess and never reuse a value meant for another kind.",`Then call \`${h}\` with api: "${e.api}" and scheme set to the name they picked to get a link where they enter it.`].join(`
3
- `)}async function u({kv:e,subject:t,baseUrl:n,target:i,auth:r,hasExistingSecret:a}){const o=await T.createElicitation(e,{subject:t,api:i.api,name:i.name,scheme:i.scheme,...i.schemeLabel?{schemeLabel:i.schemeLabel}:{},auth:r,hasExistingSecret:a});return{token:o,url:P(n,o)}}function w(e){return e.startsWith(l)?e.slice(l.length):void 0}function y(e,t){const n=`The link works for ${m()} minutes.`,i=I(e);return t?`Open this page in a browser to replace or remove the stored credentials for ${i}. They go straight to the API and never enter the chat. ${n}`:`${k(i)} needs credentials. Open this page in a browser to paste them - they go straight to the API and never enter the chat. ${n}`}function I({name:e,schemeLabel:t}){return t?`the "${t}" scheme of the "${e}" API`:`the "${e}" API`}function k(e){return e.charAt(0).toUpperCase()+e.slice(1)}function m(){return Math.round(A/6e4)}function P(e,t){if(!e)return`/mcp/credentials?token=${t}`;try{const n=new URL("/mcp/credentials",e);return n.searchParams.set("token",t),n.toString()}catch{return`${e}/mcp/credentials?token=${t}`}}function b({target:e,reason:t,hasExistingSecret:n},i,r){const o=[`**Stop and show the user this link. They have to open it in a browser to ${n?`replace or remove the stored credentials for the "${e.name}" API`:`add credentials for the "${e.name}" API`} - nothing continues until they do.**`,"",i,"",`The link is valid ${m()} minutes and opens a form on this site. Credentials go straight to the API from there: never ask for them in the conversation and never pass them as a tool argument.`,"",t.kind==="authFailure"?`The upstream API "${e.name}" returned ${t.status} and needs credentials.`:n?`Credentials are already stored for the "${e.name}" API; the link lets the user replace or remove them.`:`The "${e.name}" API needs credentials.`,...e.schemeLabel?[`The operation authenticates with its "${e.schemeLabel}" security scheme.`]:[],r?`Once the user confirms they saved them, call this tool again with sessionHandle: "${r}" - the credentials are stored against that handle, applied server-side, and never appear in the conversation.`:"Once the user confirms they saved them, call this tool again - the credentials are applied server-side and never appear in the conversation."],s=t.kind==="authFailure"?t.otherApiNames??[]:[];if(s.length>0){const p=s.map(f=>`"${f}"`).join(", ");o.push("",`The remaining APIs (${p}) will prompt for credentials on subsequent calls.`)}return o.join(`
4
- `)}export{d as CREDENTIALS_INPUT_REQUEST_KEY,S as buildCredentialsToolHint,j as buildSchemeChoiceHint,N as buildUpstreamCredentialFallback,L as buildUpstreamCredentialInputRequired,O as isCredentialElicitationDeclined,_ as schemeLabelOf};
1
+ import{inputRequired as h,inputResponse as T}from"@modelcontextprotocol/server";import{MANAGE_API_CREDENTIALS_TOOL_NAME as l}from"../constants.js";import{secretStore as d,ELICITATION_TTL_MS as w}from"./secret-store.js";import{operationsForScheme as y,orderedSchemesOf as I}from"./upstream-auth-scheme.js";import{ANONYMOUS_SUBJECT_PREFIX as u}from"./subject.js";const m="credentials";function N(e){const t=T(e,m);return t.kind==="elicit"&&(t.action==="decline"||t.action==="cancel")}function j(e,t){return Object.keys(e).length>1?t:void 0}async function x(e){const{url:t}=await p(e);return h({inputRequests:{[m]:h.elicitUrl({message:P(e.target,e.hasExistingSecret),url:t})}})}async function R(e){const{url:t}=await p(e);return S(e,t,A(e.subject))}function U(e,t){const r=e.status===401?`The stored credentials for "${e.name}" were rejected (HTTP 401).`:`The "${e.name}" API rejected the request with HTTP ${e.status}.`,n=t?`api: "${e.api}" and scheme: "${t}"`:`api: "${e.api}"`;return[r,"",`Do not retry the same call. Call \`${l}\` with ${n} to get a link the user has to open in a browser to replace or remove the stored credentials, then retry once they confirm they saved them.`].join(`
2
+ `)}function F(e,t){const r=t.map(({name:n,label:o,description:a})=>`- ${n} \u2014 ${o}${a?`: ${a}`:""}`);return[`The "${e.name}" API rejected the request with HTTP ${e.status} and this operation accepts more than one kind of credential:`,"",...r,"","Ask the user which one of these they have, and wait for the answer. Never guess and never reuse a value meant for another kind.",`Then call \`${l}\` with api: "${e.api}" and scheme set to the name they picked to get a link where they enter it.`].join(`
3
+ `)}async function p({kv:e,subject:t,baseUrl:r,target:n,auth:o,apiAuth:a,hasExistingSecret:i}){const s=a?await b(e,t,n.api,a):[],c=await d.createElicitation(e,{subject:t,api:n.api,name:n.name,scheme:n.scheme,...n.schemeLabel?{schemeLabel:n.schemeLabel}:{},auth:o,hasExistingSecret:i,...s.length>1?{schemes:s}:{}});return{token:c,url:E(r,c)}}async function b(e,t,r,n){const a=(await d.getSecretsForSubject(e,t))[r]??{};return I(n.schemes).map(i=>{const s=y(n,i),c=n.descriptions?.[i];return{name:i,...c?{label:c}:{},auth:n.schemes[i],hasExistingSecret:!!a[i],...s.length>0?{operations:s}:{}}})}function A(e){return e.startsWith(u)?e.slice(u.length):void 0}function P(e,t){const r=`The link works for ${f()} minutes.`,n=g(e);return t?`Open this page in a browser to replace or remove the stored credentials for ${n}. They go straight to the API and never enter the chat. ${r}`:`${k(n)} needs credentials. Open this page in a browser to paste them - they go straight to the API and never enter the chat. ${r}`}function g({name:e,schemeLabel:t}){return t?`the "${t}" scheme of the "${e}" API`:`the "${e}" API`}function k(e){return e.charAt(0).toUpperCase()+e.slice(1)}function f(){return Math.round(w/6e4)}function E(e,t){if(!e)return`/mcp/credentials?token=${t}`;try{const r=new URL("/mcp/credentials",e);return r.searchParams.set("token",t),r.toString()}catch{return`${e}/mcp/credentials?token=${t}`}}function S({target:e,reason:t,hasExistingSecret:r},n,o){const i=[`**Stop and show the user this link. They have to open it in a browser to ${r?`replace or remove the stored credentials for the "${e.name}" API`:`add credentials for the "${e.name}" API`} - nothing continues until they do.**`,"",n,"",`The link is valid ${f()} minutes and opens a form on this site. Credentials go straight to the API from there: never ask for them in the conversation and never pass them as a tool argument.`,"",t.kind==="authFailure"?`The upstream API "${e.name}" returned ${t.status} and needs credentials.`:r?`Credentials are already stored for the "${e.name}" API; the link lets the user replace or remove them.`:`The "${e.name}" API needs credentials.`,...e.schemeLabel?[`The operation authenticates with its "${e.schemeLabel}" security scheme.`]:[],o?`Once the user confirms they saved them, call this tool again with sessionHandle: "${o}" - the credentials are stored against that handle, applied server-side, and never appear in the conversation.`:"Once the user confirms they saved them, call this tool again - the credentials are applied server-side and never appear in the conversation."],s=t.kind==="authFailure"?t.otherApiNames??[]:[];if(s.length>0){const c=s.map($=>`"${$}"`).join(", ");i.push("",`The remaining APIs (${c}) will prompt for credentials on subsequent calls.`)}return i.join(`
4
+ `)}export{m as CREDENTIALS_INPUT_REQUEST_KEY,U as buildCredentialsToolHint,F as buildSchemeChoiceHint,R as buildUpstreamCredentialFallback,x as buildUpstreamCredentialInputRequired,N as isCredentialElicitationDeclined,j as schemeLabelOf};
@@ -7,6 +7,13 @@ export type StoredSecret = {
7
7
  savedAt?: number;
8
8
  };
9
9
  export type UpstreamSecrets = Record<string, Record<string, StoredSecret>>;
10
+ export type ElicitationScheme = {
11
+ name: string;
12
+ label?: string;
13
+ auth: UpstreamAuthScheme;
14
+ hasExistingSecret: boolean;
15
+ operations?: string[];
16
+ };
10
17
  export type ElicitationTarget = {
11
18
  subject: string;
12
19
  api: string;
@@ -15,6 +22,7 @@ export type ElicitationTarget = {
15
22
  schemeLabel?: string;
16
23
  auth: UpstreamAuthScheme;
17
24
  hasExistingSecret?: boolean;
25
+ schemes?: ElicitationScheme[];
18
26
  };
19
27
  export declare class SecretStore {
20
28
  #private;
@@ -17,6 +17,7 @@ export type UpstreamAuthScheme = {
17
17
  };
18
18
  export type UpstreamAuthRoute = {
19
19
  pattern: RegExp;
20
+ path: string;
20
21
  method: string;
21
22
  schemes: string[];
22
23
  };
@@ -37,5 +38,6 @@ export declare function resolveUpstreamApiAuth(context: McpToolContext, apiRelat
37
38
  export declare function schemeChoicesOf(auth: UpstreamApiAuth, names: readonly string[]): SchemeChoice[];
38
39
  export declare function credentialHeadersOf(schemes: Record<string, UpstreamAuthScheme>): string[];
39
40
  export declare function orderedSchemesOf(schemes: Record<string, UpstreamAuthScheme>): string[];
41
+ export declare function operationsForScheme(auth: UpstreamApiAuth, scheme: string): string[];
40
42
  export {};
41
43
  //# sourceMappingURL=upstream-auth-scheme.d.ts.map
@@ -1,2 +1,2 @@
1
- import{getApiDescriptionFromFs as O}from"../docs-mcp/tools/openapi/utils.js";import{OPERATION_METHODS as A}from"../docs-mcp/tools/openapi/constants.js";const S=120,l="default",g={kind:"raw"},h=["oauth2ClientCredentials","bearer","apiKey","cookie","raw"],C=new Set(A),E=/\{[^}]*\}/g;function c(){return{schemes:{[l]:g},defaultScheme:l,routes:[]}}async function L(e,t,n=O){const r=Object.values(e.apiDescriptionsMap??{}).find(i=>i.relativePath===t);if(!r)return c();try{const i=await n({relativePath:r.relativePath,outdir:e.outdir||"",user:e.user,rbac:e.config?.access?.rbac||{},requiresLogin:!!e.config?.access?.requiresLogin});return i?P(i,r.servers??[]):c()}catch{return c()}}function P(e,t){const n=T(e),r=Object.keys(n);if(r.length===0)return c();const i=m(e.security,n)[0]??a(r,n)[0],o=x(e,r);return{schemes:n,defaultScheme:i,routes:H(e,n,t),...Object.keys(o).length>0?{descriptions:o}:{}}}function N(e,t){return t.flatMap(n=>{const r=e.schemes[n];if(!r)return[];const i=e.descriptions?.[n];return[{name:n,label:w(r),...i?{description:i}:{}}]})}function U(e){return[...new Set(Object.values(e).map(j))]}function q(e){return a(Object.keys(e),e)}function j(e){return e.kind==="apiKey"?e.header:e.kind==="cookie"?"Cookie":"Authorization"}function w(e){switch(e.kind){case"bearer":return"API key or bearer token";case"cookie":return`browser session cookie (${e.name})`;case"apiKey":return`API key in the ${e.header} header`;case"oauth2ClientCredentials":return"OAuth2 client credentials";default:return"Authorization header value"}}function x(e,t){const n={};for(const r of t){const i=e.components?.securitySchemes?.[r],o=k(i)?i.description:void 0;if(typeof o!="string")continue;const s=o.split(`
2
- `)[0].trim();s&&(n[r]=s.slice(0,S))}return n}function T(e){const t={};for(const[n,r]of Object.entries(e.components?.securitySchemes??{})){if(!k(r))continue;const i=v(r);i&&(t[n]=i)}return t}function v(e){if(e.type==="oauth2"){const t=e.flows?.clientCredentials;return t?{kind:"oauth2ClientCredentials",tokenUrl:t.tokenUrl,scopes:Object.keys(t.scopes??{})}:{kind:"bearer"}}if(e.type==="http"&&e.scheme==="bearer")return{kind:"bearer"};if(e.type==="apiKey"&&e.in==="header")return{kind:"apiKey",header:e.name||"X-Api-Key"};if(e.type==="apiKey"&&e.in==="cookie"&&e.name)return{kind:"cookie",name:e.name}}function a(e,t){return[...e].sort((n,r)=>h.indexOf(t[n].kind)-h.indexOf(t[r].kind)||e.indexOf(n)-e.indexOf(r))}function m(e,t){const n=[];for(const r of e??[])for(const i of Object.keys(r))t[i]&&!n.includes(i)&&n.push(i);return n}function H(e,t,n){const r=R(n),i=[];for(const[o,s]of Object.entries(e.paths??{})){if(!s||typeof s!="object")continue;const f=I(r,o);if(f)for(const[p,b]of Object.entries(s)){if(!C.has(p.toLowerCase()))continue;const u=b?.security??e.security;if(u===void 0)continue;const d=m(u,t);d.length===0&&u.length>0||i.push({pattern:f,method:p.toUpperCase(),schemes:a(d,t)})}}return i}function I(e,t){if(t.startsWith("/"))return new RegExp(`^${e}${y(t)}/?$`)}function y(e){return e.split(E).map($).join("[^/]+").replace(/\/+$/,"")}function R(e){const t=[...new Set(e.map(n=>_(n.url)).filter(Boolean).map(y))];return t.length>0?`(?:${t.join("|")})?`:""}function _(e){if(!e)return"";const t=e.replace(/^[a-z][a-z0-9+.-]*:\/\//i,""),n=t.indexOf("/");return n===-1?"":t.slice(n).split(/[?#]/)[0].replace(/\/+$/,"")}function $(e){return e.replace(/[.*+?^${}()|[\]\\]/g,"\\$&")}function k(e){return typeof e=="object"&&e!==null&&"type"in e}export{l as DEFAULT_SCHEME_NAME,U as credentialHeadersOf,q as orderedSchemesOf,L as resolveUpstreamApiAuth,N as schemeChoicesOf};
1
+ import{getApiDescriptionFromFs as O}from"../docs-mcp/tools/openapi/utils.js";import{OPERATION_METHODS as S}from"../docs-mcp/tools/openapi/constants.js";const A=120,l="default",g={kind:"raw"},h=["oauth2ClientCredentials","bearer","apiKey","cookie","raw"],w=new Set(S),C=/\{[^}]*\}/g;function c(){return{schemes:{[l]:g},defaultScheme:l,routes:[]}}async function L(e,t,n=O){const r=Object.values(e.apiDescriptionsMap??{}).find(i=>i.relativePath===t);if(!r)return c();try{const i=await n({relativePath:r.relativePath,outdir:e.outdir||"",user:e.user,rbac:e.config?.access?.rbac||{},requiresLogin:!!e.config?.access?.requiresLogin});return i?E(i,r.servers??[]):c()}catch{return c()}}function E(e,t){const n=$(e),r=Object.keys(n);if(r.length===0)return c();const i=m(e.security,n)[0]??a(r,n)[0],o=x(e,r);return{schemes:n,defaultScheme:i,routes:v(e,n,t),...Object.keys(o).length>0?{descriptions:o}:{}}}function N(e,t){return t.flatMap(n=>{const r=e.schemes[n];if(!r)return[];const i=e.descriptions?.[n];return[{name:n,label:j(r),...i?{description:i}:{}}]})}function U(e){return[...new Set(Object.values(e).map(P))]}function q(e){return a(Object.keys(e),e)}function z(e,t){const n=new Set;for(const r of e.routes)r.schemes.includes(t)&&n.add(`${r.method} ${r.path}`);return[...n]}function P(e){return e.kind==="apiKey"?e.header:e.kind==="cookie"?"Cookie":"Authorization"}function j(e){switch(e.kind){case"bearer":return"API key or bearer token";case"cookie":return`browser session cookie (${e.name})`;case"apiKey":return`API key in the ${e.header} header`;case"oauth2ClientCredentials":return"OAuth2 client credentials";default:return"Authorization header value"}}function x(e,t){const n={};for(const r of t){const i=e.components?.securitySchemes?.[r],o=k(i)?i.description:void 0;if(typeof o!="string")continue;const s=o.split(`
2
+ `)[0].trim();s&&(n[r]=s.slice(0,A))}return n}function $(e){const t={};for(const[n,r]of Object.entries(e.components?.securitySchemes??{})){if(!k(r))continue;const i=T(r);i&&(t[n]=i)}return t}function T(e){if(e.type==="oauth2"){const t=e.flows?.clientCredentials;return t?{kind:"oauth2ClientCredentials",tokenUrl:t.tokenUrl,scopes:Object.keys(t.scopes??{})}:{kind:"bearer"}}if(e.type==="http"&&e.scheme==="bearer")return{kind:"bearer"};if(e.type==="apiKey"&&e.in==="header")return{kind:"apiKey",header:e.name||"X-Api-Key"};if(e.type==="apiKey"&&e.in==="cookie"&&e.name)return{kind:"cookie",name:e.name}}function a(e,t){return[...e].sort((n,r)=>h.indexOf(t[n].kind)-h.indexOf(t[r].kind)||e.indexOf(n)-e.indexOf(r))}function m(e,t){const n=[];for(const r of e??[])for(const i of Object.keys(r))t[i]&&!n.includes(i)&&n.push(i);return n}function v(e,t,n){const r=I(n),i=[];for(const[o,s]of Object.entries(e.paths??{})){if(!s||typeof s!="object")continue;const f=H(r,o);if(f)for(const[p,b]of Object.entries(s)){if(!w.has(p.toLowerCase()))continue;const u=b?.security??e.security;if(u===void 0)continue;const d=m(u,t);d.length===0&&u.length>0||i.push({pattern:f,path:o,method:p.toUpperCase(),schemes:a(d,t)})}}return i}function H(e,t){if(t.startsWith("/"))return new RegExp(`^${e}${y(t)}/?$`)}function y(e){return e.split(C).map(_).join("[^/]+").replace(/\/+$/,"")}function I(e){const t=[...new Set(e.map(n=>R(n.url)).filter(Boolean).map(y))];return t.length>0?`(?:${t.join("|")})?`:""}function R(e){if(!e)return"";const t=e.replace(/^[a-z][a-z0-9+.-]*:\/\//i,""),n=t.indexOf("/");return n===-1?"":t.slice(n).split(/[?#]/)[0].replace(/\/+$/,"")}function _(e){return e.replace(/[.*+?^${}()|[\]\\]/g,"\\$&")}function k(e){return typeof e=="object"&&e!==null&&"type"in e}export{l as DEFAULT_SCHEME_NAME,U as credentialHeadersOf,z as operationsForScheme,q as orderedSchemesOf,L as resolveUpstreamApiAuth,N as schemeChoicesOf};
@@ -1,144 +1 @@
1
- import{secretStore as c,ELICITATION_TTL_MS as C}from"../gateway-mcp/secret-store.js";import{rejectCredential as z}from"../gateway-mcp/credential-validation.js";import{buildAllowedHostMatchers as F}from"../gateway-mcp/fetch/allowed-hosts.js";import{assertUrlAllowed as O}from"../gateway-mcp/fetch/ssrf-guard.js";import{readCappedBody as j,getDefaultAgent as B}from"../gateway-mcp/fetch/fetch-bridge.js";const p={"Content-Type":"text/html; charset=utf-8","Cache-Control":"no-store","X-Frame-Options":"DENY","Referrer-Policy":"no-referrer","Content-Security-Policy":"default-src 'none'; style-src 'unsafe-inline'; form-action 'self'"},M=3e4,q=64*1024;let I=fetch;function re(e){I=e}const H=async(e,t)=>{const n=await t.getKv();if(e.method==="GET"){const o=new URL(e.url).searchParams.get("token")??"",r=await c.peekElicitation(n,o);return r?new Response(S(r,o),{status:200,headers:p}):m()}if(e.method==="POST"){const o=await e.formData(),r=f(o,"token"),s=f(o,"mode")||"token";return s==="client-credentials"?G(n,o,r):s==="clear"?D(n,r):U(n,o,r)}return new Response("Method Not Allowed",{status:405,headers:{Allow:"GET, POST"}})};async function U(e,t,n){const o=f(t,"secret").trim(),r=await c.peekElicitation(e,n);if(!r)return m();const s=z(r.auth,o);if(s)return P(r,n,s);const a=await c.consumeElicitation(e,n);return a?(await c.setSecret(e,a.subject,a.api,a.scheme,K(a.auth,o)),new Response(E(a),{status:200,headers:p})):m()}async function D(e,t){const n=await c.consumeElicitation(e,t);return n?(await c.deleteSecret(e,n.subject,n.api,n.scheme),new Response(V(n),{status:200,headers:p})):m()}async function G(e,t,n){const o=f(t,"client_id").trim(),r=f(t,"client_secret").trim(),s=await c.peekElicitation(e,n);if(!s||s.auth.kind!=="oauth2ClientCredentials")return m();if(!o||!r)return P(s,n,"Enter both the client ID and the client secret before saving.");const a=await c.consumeElicitation(e,n);if(!a||a.auth.kind!=="oauth2ClientCredentials")return m();const g=a.auth;try{const T=F([{url:g.tokenUrl}]),_=O(g.tokenUrl,T),w=new AbortController,R=setTimeout(()=>w.abort(),M);let v;try{v=await I(_,{method:"POST",headers:{"content-type":"application/x-www-form-urlencoded"},body:new URLSearchParams({grant_type:"client_credentials",client_id:o,client_secret:r,...g.scopes.length>0?{scope:g.scopes.join(" ")}:{}}),redirect:"manual",signal:w.signal,dispatcher:B()})}finally{clearTimeout(R)}if(!v.ok)return y(a.name);let $;try{$=await j(v,q)}catch{return y(a.name)}let h;try{h=JSON.parse($)}catch{return y(a.name)}const x=typeof h=="object"&&h!==null&&"access_token"in h?h.access_token:void 0,A=typeof x=="string"?x.trim():"";return A?(await c.setSecret(e,a.subject,a.api,a.scheme,{header:"Authorization",value:`Bearer ${A}`}),new Response(E(a),{status:200,headers:p})):y(a.name)}catch{return y(a.name)}}function f(e,t){const n=e.get(t);return typeof n=="string"?n:""}function P(e,t,n){return new Response(S(e,t,n),{status:400,headers:p})}function K(e,t){return e.kind==="apiKey"?{header:e.header,value:t}:e.kind==="cookie"?{header:"Cookie",value:`${e.name}=${t}`}:e.kind==="bearer"||e.kind==="oauth2ClientCredentials"?{header:"Authorization",value:L(t)}:{header:"Authorization",value:t}}function L(e){const t=e.trim();return t.toLowerCase().startsWith("bearer")?t:`Bearer ${t}`}function i(e){return e.replaceAll("&","&amp;").replaceAll("<","&lt;").replaceAll(">","&gt;").replaceAll('"',"&quot;")}function d(e){return`<!doctype html>
2
- <html lang="en">
3
- <head>
4
- <meta charset="utf-8">
5
- <meta name="viewport" content="width=device-width, initial-scale=1">
6
- <meta name="robots" content="noindex">
7
- <title>API credentials</title>
8
- <style>
9
- :root{color-scheme:light dark}
10
- *{box-sizing:border-box}
11
- body{font-family:-apple-system,BlinkMacSystemFont,"Segoe UI",Roboto,Helvetica,Arial,sans-serif;margin:0;min-height:100vh;display:flex;align-items:center;justify-content:center;background:#f3f4f6;color:#111827;padding:1.5rem}
12
- .card{width:100%;max-width:26rem;background:#ffffff;border-radius:.75rem;box-shadow:0 10px 25px -5px rgba(0,0,0,.1),0 8px 10px -6px rgba(0,0,0,.1);padding:2rem}
13
- h1{font-size:1.25rem;margin:0 0 .5rem}
14
- .section{margin-top:1.5rem;padding-top:1.5rem;border-top:1px solid #e5e7eb}
15
- .section h2{font-size:1rem;margin:0 0 .5rem}
16
- p.hint{color:#6b7280;font-size:.875rem;line-height:1.4}
17
- p.error{margin:1rem 0 0;padding:.75rem;border:1px solid #fecaca;border-radius:.5rem;background:#fef2f2;color:#991b1b;font-size:.875rem;line-height:1.4}
18
- label{display:block;margin:1rem 0 .25rem;font-weight:600;font-size:.875rem}
19
- input[type=password],input[type=text]{width:100%;padding:.625rem .75rem;font-size:1rem;border:1px solid #d1d5db;border-radius:.5rem;background:#fff;color:#111827}
20
- input:focus{outline:none;border-color:#4f46e5;box-shadow:0 0 0 3px rgba(79,70,229,.2)}
21
- button{margin-top:1.25rem;padding:.625rem 1.5rem;font-size:1rem;font-weight:600;color:#fff;background:#4f46e5;border:none;border-radius:.5rem;cursor:pointer}
22
- button:hover{background:#4338ca}
23
- button:focus-visible{outline:2px solid #4f46e5;outline-offset:2px}
24
- button.danger{background:#b91c1c}
25
- button.danger:hover{background:#991b1b}
26
- button.danger:focus-visible{outline-color:#b91c1c}
27
- code{background:#f3f4f6;padding:.125rem .375rem;border-radius:.25rem;font-size:.85em}
28
- footer{margin-top:1.5rem;padding-top:1rem;border-top:1px solid #e5e7eb;font-size:.75rem;color:#9ca3af;text-align:center}
29
- @media (prefers-color-scheme:dark){
30
- body{background:#0b0f19;color:#e5e7eb}
31
- .card{background:#111827;box-shadow:0 10px 25px -5px rgba(0,0,0,.4)}
32
- .section{border-top-color:#1f2937}
33
- p.hint{color:#9ca3af}
34
- p.error{border-color:#7f1d1d;background:#2a1215;color:#fca5a5}
35
- input[type=password],input[type=text]{background:#0b0f19;border-color:#374151;color:#e5e7eb}
36
- code{background:#1f2937}
37
- footer{border-top-color:#1f2937;color:#6b7280}
38
- }
39
- </style>
40
- </head>
41
- <body><div class="card">${e}</div></body>
42
- </html>`}function l(e){return`<h1>\u{1F512} ${i(e)}</h1>`}function u(){return"<footer>Secured by Redocly &middot; credentials never enter the AI conversation</footer>"}function b(){return`<p class="hint">This link expires ${Math.round(C/6e4)} minutes after it was issued. If it stops working, ask your AI client for a new one.</p>`}function S(e,t,n){const o=e.hasExistingSecret?N(t):"",r=n?`<p class="error">${i(n)}</p>`:"",s=k(e);switch(e.auth.kind){case"bearer":return W(s,t,o,r);case"apiKey":return X(s,t,e.auth.header,o,r);case"cookie":return Y(s,t,e.auth.name,o,r);case"oauth2ClientCredentials":return J(s,t,e.auth.scopes,o,r);default:return Q(s,t,o,r)}}function k({name:e,schemeLabel:t}){return t?`${e} (${t})`:e}function N(e){return`
43
- <div class="section">
44
- <h2>Remove stored credentials</h2>
45
- <p class="hint">This API already has credentials stored. Save above to replace them, or remove them to make calls anonymous again.</p>
46
- <form method="post" action="/mcp/credentials" autocomplete="off">
47
- <input type="hidden" name="token" value="${i(e)}">
48
- <input type="hidden" name="mode" value="clear">
49
- <button class="danger" type="submit">Remove credentials</button>
50
- </form>
51
- </div>`}function W(e,t,n,o){return d(`
52
- ${l(`Credentials for ${e}`)}
53
- ${o}
54
- <p class="hint">Stored encrypted for you only (24 hours for anonymous callers, 30 days when signed in) and sent to the API as the <code>Authorization</code> header. It never passes through the AI conversation.</p>
55
- <form method="post" action="/mcp/credentials" autocomplete="off">
56
- <input type="hidden" name="token" value="${i(t)}">
57
- <input type="hidden" name="mode" value="token">
58
- <label for="secret">API token</label>
59
- <input type="password" id="secret" name="secret" placeholder="your API token" required autofocus>
60
- <p class="hint">Paste the raw token or one already prefixed with <code>Bearer</code> &mdash; we add the prefix automatically if it is missing.</p>
61
- <button type="submit">Save credentials</button>
62
- </form>
63
- ${n}
64
- ${b()}
65
- ${u()}`)}function X(e,t,n,o,r){return d(`
66
- ${l(`Credentials for ${e}`)}
67
- ${r}
68
- <p class="hint">Stored encrypted for you only (24 hours for anonymous callers, 30 days when signed in) and sent to the API as the <code>${i(n)}</code> header. It never passes through the AI conversation.</p>
69
- <form method="post" action="/mcp/credentials" autocomplete="off">
70
- <input type="hidden" name="token" value="${i(t)}">
71
- <input type="hidden" name="mode" value="token">
72
- <label for="secret">${i(n)} value</label>
73
- <input type="password" id="secret" name="secret" placeholder="value" required autofocus>
74
- <button type="submit">Save credentials</button>
75
- </form>
76
- ${o}
77
- ${b()}
78
- ${u()}`)}function Y(e,t,n,o,r){return d(`
79
- ${l(`Credentials for ${e}`)}
80
- ${r}
81
- <p class="hint">Stored encrypted for you only (24 hours for anonymous callers, 30 days when signed in) and sent to the API as the <code>${i(n)}</code> cookie. It never passes through the AI conversation.</p>
82
- <form method="post" action="/mcp/credentials" autocomplete="off">
83
- <input type="hidden" name="token" value="${i(t)}">
84
- <input type="hidden" name="mode" value="token">
85
- <label for="secret">${i(n)} cookie value</label>
86
- <input type="password" id="secret" name="secret" placeholder="value" required autofocus>
87
- <p class="hint">Paste only the value &mdash; we send it as <code>${i(n)}=&lt;value&gt;</code>.</p>
88
- <button type="submit">Save credentials</button>
89
- </form>
90
- ${o}
91
- ${b()}
92
- ${u()}`)}function J(e,t,n,o,r){const s=n.length>0?n.map(i).join(", "):"default scopes";return d(`
93
- ${l(`Credentials for ${e}`)}
94
- ${r}
95
- <p class="hint">Paste a token you already have, or let us exchange client credentials for one via OAuth2 client_credentials.</p>
96
- <div class="section">
97
- <h2>I have a token</h2>
98
- <form method="post" action="/mcp/credentials" autocomplete="off">
99
- <input type="hidden" name="token" value="${i(t)}">
100
- <input type="hidden" name="mode" value="token">
101
- <label for="secret">API token</label>
102
- <input type="password" id="secret" name="secret" placeholder="your API token" required autofocus>
103
- <p class="hint">We add <code>Bearer</code> automatically if it is missing.</p>
104
- <button type="submit">Save token</button>
105
- </form>
106
- </div>
107
- <div class="section">
108
- <h2>Exchange client credentials</h2>
109
- <p class="hint">We request a token from the API on your behalf. Scopes: ${s}.</p>
110
- <form method="post" action="/mcp/credentials" autocomplete="off">
111
- <input type="hidden" name="token" value="${i(t)}">
112
- <input type="hidden" name="mode" value="client-credentials">
113
- <label for="client_id">Client ID</label>
114
- <input type="text" id="client_id" name="client_id" required>
115
- <label for="client_secret">Client secret</label>
116
- <input type="password" id="client_secret" name="client_secret" required>
117
- <button type="submit">Exchange &amp; save</button>
118
- </form>
119
- </div>
120
- ${o}
121
- ${b()}
122
- ${u()}`)}function Q(e,t,n,o){return d(`
123
- ${l(`Credentials for ${e}`)}
124
- ${o}
125
- <p class="hint">The value is stored encrypted for you only (24 hours for anonymous callers, 30 days when signed in) and is sent to the API as the <code>Authorization</code> header. It never passes through the AI conversation.</p>
126
- <form method="post" action="/mcp/credentials" autocomplete="off">
127
- <input type="hidden" name="token" value="${i(t)}">
128
- <input type="hidden" name="mode" value="token">
129
- <label for="secret">Authorization header value</label>
130
- <input type="password" id="secret" name="secret" placeholder="Bearer &lt;your token&gt;" required autofocus>
131
- <button type="submit">Save credentials</button>
132
- </form>
133
- ${n}
134
- ${b()}
135
- ${u()}`)}function V(e){return d(`
136
- ${l("Credentials removed")}
137
- <p>The stored credentials for ${i(k(e))} are gone. Calls to that API are anonymous again until you add new ones.</p>
138
- ${u()}`)}function E(e){return d(`
139
- ${l("Credentials saved")}
140
- <p>Credentials for ${i(k(e))} are saved. Go back to your AI client and ask it to retry - or confirm the prompt it left open, if there is one. You can close this tab.</p>
141
- ${u()}`)}function y(e){return new Response(d(`
142
- ${l("Exchange failed")}
143
- <p>We could not exchange your client credentials for a token for ${i(e)}. This link has been used up - go back to your AI client and request a new link, then try again.</p>
144
- ${u()}`),{status:502,headers:p})}function m(){return new Response(d(`${l("Link expired")}<p>This credentials link is invalid, already used, or older than ${Math.round(C/6e4)} minutes. Retry the request in your AI client to get a fresh link.</p>${u()}`),{status:404,headers:p})}var ae=H;export{ae as default,re as setCredentialsFetchImpl};
1
+ import{secretStore as u}from"../gateway-mcp/secret-store.js";import{rejectCredential as k}from"../gateway-mcp/credential-validation.js";import{buildAllowedHostMatchers as P}from"../gateway-mcp/fetch/allowed-hosts.js";import{assertUrlAllowed as A}from"../gateway-mcp/fetch/ssrf-guard.js";import{readCappedBody as R,getDefaultAgent as j}from"../gateway-mcp/fetch/fetch-bridge.js";import{logger as O}from"../../../tools/notifiers/logger.js";import{clearedPage as v,formPage as C,notFoundPage as N,successPage as y}from"./mcp-credentials-page.js";const m={"Content-Type":"text/html; charset=utf-8","Cache-Control":"no-store","X-Frame-Options":"DENY","Referrer-Policy":"no-referrer","Content-Security-Policy":"default-src 'none'; style-src 'unsafe-inline'; form-action 'self'"},I=3e4,D=64*1024;let x=fetch;function Q(e){x=e}const F=async(e,r)=>{const t=await r.getKv();if(e.method==="GET"){const n=new URL(e.url).searchParams.get("token")??"",o=await u.peekElicitation(t,n);return o?new Response(C(o,n),{status:200,headers:m}):f()}if(e.method==="POST"){const n=await e.formData(),o=d(n,"token"),s=d(n,"mode")||"token";return s==="multi"?B(t,n,o):s==="client-credentials"?G(t,n,o):s==="clear"?U(t,o):M(t,n,o)}return new Response("Method Not Allowed",{status:405,headers:{Allow:"GET, POST"}})};async function M(e,r,t){const n=d(r,"secret").trim(),o=await u.peekElicitation(e,t);if(!o)return f();const s=k(o.auth,n);if(s)return p(o,t,s);const l=await u.consumeElicitation(e,t);return l?(await u.setSecret(e,l.subject,l.api,l.scheme,S(l.auth,n)),new Response(y(l),{status:200,headers:m})):f()}async function U(e,r){const t=await u.consumeElicitation(e,r);return t?(await u.deleteSecret(e,t.subject,t.api,t.scheme),new Response(v(t),{status:200,headers:m})):f()}async function B(e,r,t){const n=await u.peekElicitation(e,t);if(!n?.schemes||n.schemes.length<=1)return f();const o=n.schemes,s=[];for(let a=0;a<o.length;a++){const i=o[a];if(i.hasExistingSecret&&d(r,`remove__${a}`)){s.push({kind:"remove",scheme:i.name});continue}const h=d(r,`secret__${a}`).trim();if(i.auth.kind==="oauth2ClientCredentials"){const E=d(r,`client_id__${a}`).trim(),_=d(r,`client_secret__${a}`).trim();if(h){const b=k(i.auth,h);if(b)return p(n,t,`${i.name}: ${b}`);s.push({kind:"set",scheme:i.name,secret:S(i.auth,h)})}else if(E&&_)s.push({kind:"exchange",scheme:i.name,auth:i.auth,clientId:E,clientSecret:_});else if(E||_)return p(n,t,`${i.name}: enter both the client ID and the client secret, or leave both blank.`);continue}if(!h)continue;const w=k(i.auth,h);if(w)return p(n,t,`${i.name}: ${w}`);s.push({kind:"set",scheme:i.name,secret:S(i.auth,h)})}if(s.length===0)return p(n,t,"Fill in at least one credential, or check a box to remove one, before saving.");const l=[];for(const a of s){if(a.kind!=="exchange"){l.push(a);continue}const i=await $(a.auth,a.clientId,a.clientSecret);if(!i.ok)return T(n,t,i.reason,a.scheme);l.push({kind:"set",scheme:a.scheme,secret:i.secret})}const c=await u.consumeElicitation(e,t);if(!c)return f();for(const a of l)a.kind==="remove"?await u.deleteSecret(e,c.subject,c.api,a.scheme):await u.setSecret(e,c.subject,c.api,a.scheme,a.secret);return new Response(y(c),{status:200,headers:m})}async function G(e,r,t){const n=d(r,"client_id").trim(),o=d(r,"client_secret").trim(),s=await u.peekElicitation(e,t);if(!s||s.auth.kind!=="oauth2ClientCredentials")return f();if(!n||!o)return p(s,t,"Enter both the client ID and the client secret before saving.");const l=await $(s.auth,n,o);if(!l.ok)return T(s,t,l.reason);const c=await u.consumeElicitation(e,t);return c?(await u.setSecret(e,c.subject,c.api,c.scheme,l.secret),new Response(y(c),{status:200,headers:m})):f()}async function $(e,r,t){try{const n=P([{url:e.tokenUrl}]),o=A(e.tokenUrl,n),s=new AbortController,l=setTimeout(()=>s.abort(),I);let c;try{c=await x(o,{method:"POST",headers:{"content-type":"application/x-www-form-urlencoded"},body:new URLSearchParams({grant_type:"client_credentials",client_id:r,client_secret:t}),redirect:"manual",signal:s.signal,dispatcher:j()})}finally{clearTimeout(l)}if(!c.ok)return g(`token endpoint returned ${c.status}`);let a;try{a=await R(c,D)}catch{return g("token response exceeded the size cap")}let i;try{i=JSON.parse(a)}catch{return g("token response was not valid JSON")}const h=typeof i=="object"&&i!==null&&"access_token"in i?i.access_token:void 0,w=typeof h=="string"?h.trim():"";return w?{ok:!0,secret:{header:"Authorization",value:`Bearer ${w}`}}:g("token response had no access_token")}catch(n){return g(n instanceof Error?n.message:String(n))}}function g(e){return{ok:!1,reason:e}}function T(e,r,t,n){return O.warn(`gateway oauth2 client_credentials exchange failed for ${e.api}: ${t}`),p(e,r,`${n?`${n}: we`:"We"} could not exchange those client credentials for a token. Check the client ID and secret, then try again.`)}function d(e,r){const t=e.get(r);return typeof t=="string"?t:""}function p(e,r,t){return new Response(C(e,r,t),{status:400,headers:m})}function f(){return new Response(N(),{status:404,headers:m})}function S(e,r){return e.kind==="apiKey"?{header:e.header,value:r}:e.kind==="cookie"?{header:"Cookie",value:`${e.name}=${r}`}:e.kind==="bearer"||e.kind==="oauth2ClientCredentials"?{header:"Authorization",value:H(r)}:{header:"Authorization",value:r}}function H(e){const r=e.trim();return r.toLowerCase().startsWith("bearer")?r:`Bearer ${r}`}var V=F;export{V as default,Q as setCredentialsFetchImpl};
@@ -0,0 +1,6 @@
1
+ import type { ElicitationTarget } from '../gateway-mcp/secret-store.js';
2
+ export declare function formPage(target: ElicitationTarget, token: string, error?: string): string;
3
+ export declare function clearedPage(target: ElicitationTarget): string;
4
+ export declare function successPage(target: ElicitationTarget): string;
5
+ export declare function notFoundPage(): string;
6
+ //# sourceMappingURL=mcp-credentials-page.d.ts.map