diff --git a/packages/gitbook/src/components/DocumentView/OpenAPI/style.css b/packages/gitbook/src/components/DocumentView/OpenAPI/style.css
index e55d75e5dc..c813da2a75 100644
--- a/packages/gitbook/src/components/DocumentView/OpenAPI/style.css
+++ b/packages/gitbook/src/components/DocumentView/OpenAPI/style.css
@@ -1039,4 +1039,12 @@ body:has(.openapi-select-popover) {
.openapi-required-scopes .openapi-required-scopes-description {
@apply text-xs !text-tint font-normal mb-2;
+}
+
+.openapi-schema-alternatives .openapi-securities-scopes {
+ @apply ml-0 pl-0;
+}
+
+.openapi-scopes-alternatives .openapi-schema-alternatives {
+ @apply flex flex-col gap-2;
}
\ No newline at end of file
diff --git a/packages/react-openapi/src/OpenAPIRequiredScopes.tsx b/packages/react-openapi/src/OpenAPIRequiredScopes.tsx
index 6bef0def09..1b36e978fe 100644
--- a/packages/react-openapi/src/OpenAPIRequiredScopes.tsx
+++ b/packages/react-openapi/src/OpenAPIRequiredScopes.tsx
@@ -24,11 +24,19 @@ export function OpenAPIRequiredScopes(props: {
return null;
}
- const scopes = selectedSecurity.schemes.flatMap((scheme) => {
- return scheme.scopes ?? [];
- });
+ const scopeAlternatives =
+ selectedSecurity.scopeAlternatives.length > 0
+ ? selectedSecurity.scopeAlternatives
+ : [
+ selectedSecurity.schemes.flatMap((scheme) => {
+ return scheme.scopes ?? [];
+ }),
+ ];
+ const resolvedAlternatives = scopeAlternatives
+ .map((scopes) => dedupeScopes(scopes))
+ .filter((scopes) => scopes.length > 0);
- if (!scopes.length) {
+ if (!resolvedAlternatives.length) {
return null;
}
@@ -51,7 +59,12 @@ export function OpenAPIRequiredScopes(props: {
{
key: 'scopes',
label: '',
- body: ,
+ body: (
+
+ ),
},
],
},
@@ -60,23 +73,57 @@ export function OpenAPIRequiredScopes(props: {
);
}
+function OpenAPIScopeAlternatives(props: {
+ alternatives: OpenAPISecurityScope[][];
+ context: OpenAPIClientContext;
+}) {
+ const { alternatives, context } = props;
+
+ if (alternatives.length === 1) {
+ return ;
+ }
+
+ return (
+
+
+ {t(context.translation, 'required_scopes_description')}
+
+
+ {alternatives.map((scopes, index) => (
+
+
+ {index < alternatives.length - 1 ? (
+
+ {t(context.translation, 'or')}
+
+ ) : null}
+
+ ))}
+
+
+ );
+}
+
export function OpenAPISchemaScopes(props: {
- scopes: OpenAPISecurityScope[];
+ scopes: OpenAPISecurityScope[] | undefined;
context: OpenAPIClientContext;
isOAuth2?: boolean;
+ hideDescription?: boolean;
}) {
- const { scopes, context, isOAuth2 } = props;
+ const { scopes, context, isOAuth2, hideDescription } = props;
return (
-
- {t(
- context.translation,
- isOAuth2 ? 'available_scopes' : 'required_scopes_description'
- )}
-
+ {!hideDescription ? (
+
+ {t(
+ context.translation,
+ isOAuth2 ? 'available_scopes' : 'required_scopes_description'
+ )}
+
+ ) : null}
- {scopes.map((scope) => (
+ {scopes?.map((scope) => (
))}
@@ -116,3 +163,18 @@ function OpenAPIScopeItemKey(props: {
);
}
+
+function dedupeScopes(scopes: OpenAPISecurityScope[]) {
+ const seen = new Set
();
+ const deduped: OpenAPISecurityScope[] = [];
+
+ for (const scope of scopes) {
+ if (seen.has(scope[0])) {
+ continue;
+ }
+ seen.add(scope[0]);
+ deduped.push(scope);
+ }
+
+ return deduped;
+}
diff --git a/packages/react-openapi/src/resolveOpenAPIOperation.ts b/packages/react-openapi/src/resolveOpenAPIOperation.ts
index 9a7c1567db..561b61ae9d 100644
--- a/packages/react-openapi/src/resolveOpenAPIOperation.ts
+++ b/packages/react-openapi/src/resolveOpenAPIOperation.ts
@@ -50,7 +50,7 @@ export async function resolveOpenAPIOperation(
const flatSecurities = flattenSecurities(security);
// Resolve securities
- const securities: OpenAPIOperationData['securities'] = [];
+ const securitiesMap = new Map();
for (const entry of flatSecurities) {
const [securityKey, operationScopes] = Object.entries(entry)[0] ?? [];
if (securityKey) {
@@ -59,14 +59,13 @@ export async function resolveOpenAPIOperation(
securityScheme,
operationScopes,
});
- securities.push([
- securityKey,
- {
- ...securityScheme,
- required: !isOptionalSecurity,
- scopes,
- },
- ]);
+ const existing = securitiesMap.get(securityKey);
+ const mergedScopes = mergeSecurityScopes(existing?.scopes ?? null, scopes);
+ securitiesMap.set(securityKey, {
+ ...securityScheme,
+ required: !isOptionalSecurity,
+ scopes: mergedScopes,
+ });
}
}
@@ -75,7 +74,7 @@ export async function resolveOpenAPIOperation(
operation: { ...operation, security },
method,
path,
- securities,
+ securities: Array.from(securitiesMap.entries()),
'x-codeSamples':
typeof schema['x-codeSamples'] === 'boolean' ? schema['x-codeSamples'] : undefined,
'x-hideTryItPanel':
@@ -176,6 +175,31 @@ function resolveSecurityScopes({
return operationScopes.map((scope) => [scope, undefined]);
}
+function mergeSecurityScopes(
+ existing: OpenAPISecurityScope[] | null,
+ incoming: OpenAPISecurityScope[] | null
+): OpenAPISecurityScope[] | null {
+ if (!existing?.length) {
+ return incoming;
+ }
+ if (!incoming?.length) {
+ return existing;
+ }
+
+ const seen = new Set();
+ const merged: OpenAPISecurityScope[] = [];
+
+ for (const scope of [...existing, ...incoming]) {
+ if (seen.has(scope[0])) {
+ continue;
+ }
+ seen.add(scope[0]);
+ merged.push(scope);
+ }
+
+ return merged;
+}
+
/**
* Check if a security scheme is an OAuth or OpenID Connect security scheme.
*/
diff --git a/packages/react-openapi/src/utils.ts b/packages/react-openapi/src/utils.ts
index 72b3ca60b4..3d35d080f8 100644
--- a/packages/react-openapi/src/utils.ts
+++ b/packages/react-openapi/src/utils.ts
@@ -2,7 +2,11 @@ import type { AnyObject, OpenAPIV3, OpenAPIV3_1 } from '@gitbook/openapi-parser'
import type { OpenAPIUniversalContext } from './context';
import { stringifyOpenAPI } from './stringifyOpenAPI';
import { tString } from './translate';
-import type { OpenAPICustomSecurityScheme, OpenAPIOperationData } from './types';
+import type {
+ OpenAPICustomSecurityScheme,
+ OpenAPIOperationData,
+ OpenAPISecurityScope,
+} from './types';
export function checkIsReference(input: unknown): input is OpenAPIV3.ReferenceObject {
return typeof input === 'object' && !!input && '$ref' in input;
@@ -327,6 +331,7 @@ export type OperationSecurityInfo = {
key: string;
label: string;
schemes: OpenAPICustomSecurityScheme[];
+ scopeAlternatives: OpenAPISecurityScope[][];
};
/**
@@ -345,18 +350,61 @@ export function extractOperationSecurityInfo(args: {
key,
label: key,
schemes: [security],
+ scopeAlternatives: security.scopes?.length ? [security.scopes] : [],
}));
}
- return securityRequirement.map((requirement, idx) => {
- const schemeKeys = Object.keys(requirement);
+ const grouped = new Map();
- return {
- key: `security-${idx}`,
- label: schemeKeys.join(' & '),
- schemes: schemeKeys
- .map((schemeKey) => securitiesMap.get(schemeKey))
- .filter((s): s is OpenAPICustomSecurityScheme => s !== undefined),
- };
+ securityRequirement.forEach((requirement) => {
+ const schemeKeys = Object.keys(requirement);
+ const label = schemeKeys.join(' & ');
+ const existing = grouped.get(label);
+ const schemes = schemeKeys
+ .map((schemeKey) => securitiesMap.get(schemeKey))
+ .filter((s): s is OpenAPICustomSecurityScheme => s !== undefined);
+ const scopesForRequirement = schemeKeys.flatMap((schemeKey) =>
+ resolveRequiredScopesForScheme(securitiesMap.get(schemeKey), requirement[schemeKey])
+ );
+
+ if (existing) {
+ existing.scopeAlternatives.push(scopesForRequirement);
+ if (!existing.schemes.length && schemes.length) {
+ existing.schemes = schemes;
+ }
+ } else {
+ grouped.set(label, {
+ key: `security-${grouped.size}`,
+ label,
+ schemes,
+ scopeAlternatives: [scopesForRequirement],
+ });
+ }
});
+
+ return Array.from(grouped.values());
+}
+
+function resolveRequiredScopesForScheme(
+ security: OpenAPICustomSecurityScheme | undefined,
+ operationScopes: string[] | undefined
+): OpenAPISecurityScope[] {
+ if (!security || !operationScopes?.length) {
+ return [];
+ }
+
+ if (security.type === 'oauth2') {
+ const flows = security.flows ? Object.entries(security.flows) : [];
+ const resolved = flows.flatMap(([_, flow]) => {
+ return Object.entries(flow.scopes ?? {}).filter(([scope]) =>
+ operationScopes.includes(scope)
+ );
+ });
+
+ if (resolved.length) {
+ return resolved;
+ }
+ }
+
+ return operationScopes.map((scope) => [scope, undefined]);
}