mirror of
https://github.com/GitbookIO/gitbook.git
synced 2026-09-12 05:48:57 +00:00
Adapt OpenAPI to eval adaptive exprs & prefill TryIt config (#3607)
This commit is contained in:
@@ -0,0 +1,7 @@
|
||||
---
|
||||
"@gitbook/react-openapi": minor
|
||||
"gitbook": minor
|
||||
"@gitbook/openapi-parser": patch
|
||||
---
|
||||
|
||||
Adapt OpenAPI blocks to eval adaptive exprs & prefill TryIt config
|
||||
@@ -99,6 +99,7 @@
|
||||
"@gitbook/cache-tags": "workspace:*",
|
||||
"@gitbook/colors": "workspace:*",
|
||||
"@gitbook/emoji-codepoints": "workspace:*",
|
||||
"@gitbook/expr": "workspace:*",
|
||||
"@gitbook/fonts": "workspace:*",
|
||||
"@gitbook/icons": "workspace:*",
|
||||
"@gitbook/openapi-parser": "workspace:*",
|
||||
@@ -262,9 +263,11 @@
|
||||
"name": "@gitbook/react-openapi",
|
||||
"version": "1.3.6",
|
||||
"dependencies": {
|
||||
"@gitbook/expr": "workspace:*",
|
||||
"@gitbook/openapi-parser": "workspace:*",
|
||||
"@scalar/api-client-react": "^1.3.16",
|
||||
"@scalar/oas-utils": "^0.2.130",
|
||||
"@scalar/types": "^0.1.9",
|
||||
"clsx": "^2.1.1",
|
||||
"flatted": "^3.2.9",
|
||||
"js-yaml": "^4.1.0",
|
||||
@@ -1173,7 +1176,7 @@
|
||||
|
||||
"@scalar/themes": ["@scalar/themes@0.9.86", "", { "dependencies": { "@scalar/types": "0.1.7" } }, "sha512-QUHo9g5oSWi+0Lm1vJY9TaMZRau8LHg+vte7q5BVTBnu6NuQfigCaN+ouQ73FqIVd96TwMO6Db+dilK1B+9row=="],
|
||||
|
||||
"@scalar/types": ["@scalar/types@0.2.3", "", { "dependencies": { "@scalar/openapi-types": "0.3.3", "nanoid": "^5.1.5", "zod": "3.24.1" } }, "sha512-K1/Vp5xaQ8TEGDYwHg88OBuQWJRWrE3ToZ0Z2LJwskcY1Eg+uDiRQXKF4QPngZzu5rrWgRErAq4yte4Y7omqgA=="],
|
||||
"@scalar/types": ["@scalar/types@0.1.16", "", { "dependencies": { "@scalar/openapi-types": "0.2.3", "nanoid": "^5.1.5", "zod": "3.24.1" } }, "sha512-v1L96F8Inn27NUw5Xcl0iifw3SOX0R9WBV6GeswR23s8i1SJz7UiLEi8rLOqOB5DiLtC8hDK0kNl4TKn7MNJWQ=="],
|
||||
|
||||
"@scalar/use-codemirror": ["@scalar/use-codemirror@0.12.13", "", { "dependencies": { "@codemirror/autocomplete": "^6.18.3", "@codemirror/commands": "^6.7.1", "@codemirror/lang-css": "^6.3.1", "@codemirror/lang-html": "^6.4.8", "@codemirror/lang-json": "^6.0.0", "@codemirror/lang-xml": "^6.0.0", "@codemirror/lang-yaml": "^6.1.2", "@codemirror/language": "^6.10.7", "@codemirror/lint": "^6.8.4", "@codemirror/state": "^6.5.0", "@codemirror/view": "^6.35.3", "@lezer/common": "^1.2.3", "@lezer/highlight": "^1.2.1", "@replit/codemirror-css-color-picker": "^6.3.0", "@scalar/components": "0.14.12", "codemirror": "^6.0.0", "vue": "^3.5.12" } }, "sha512-XOXCUT3b1l0gkp4CY51e7xMgR6IP/AMpYIZ97fBOOjnrUIJ8B/J+U/BMS6+lPKiN0hexaSALRWlVBf90txdT8w=="],
|
||||
|
||||
@@ -3963,10 +3966,14 @@
|
||||
|
||||
"@scalar/api-client/@scalar/themes": ["@scalar/themes@0.13.4", "", { "dependencies": { "@scalar/types": "0.2.3", "nanoid": "^5.1.5" } }, "sha512-PMiSqX+MZF29u32ogEjTVA5c/LBd5cE/QPF7lK0EJeemXRy2r/nWbvrXtqD9Y7Sw54g1caVNjK8yJcCYJdB6hw=="],
|
||||
|
||||
"@scalar/api-client/@scalar/types": ["@scalar/types@0.2.3", "", { "dependencies": { "@scalar/openapi-types": "0.3.3", "nanoid": "^5.1.5", "zod": "3.24.1" } }, "sha512-K1/Vp5xaQ8TEGDYwHg88OBuQWJRWrE3ToZ0Z2LJwskcY1Eg+uDiRQXKF4QPngZzu5rrWgRErAq4yte4Y7omqgA=="],
|
||||
|
||||
"@scalar/api-client/pretty-ms": ["pretty-ms@8.0.0", "", { "dependencies": { "parse-ms": "^3.0.0" } }, "sha512-ASJqOugUF1bbzI35STMBUpZqdfYKlJugy6JBziGi2EE+AL5JPJGSzvpeVXojxrr0ViUYoToUjb5kjSEGf7Y83Q=="],
|
||||
|
||||
"@scalar/api-client/zod": ["zod@3.24.1", "", {}, "sha512-muH7gBL9sI1nciMZV67X5fTKKBLtwpZ5VBp1vsOQzj1MhrBZ4wlVCm3gedKZWLp0Oyel8sIGfeiz54Su+OVT+A=="],
|
||||
|
||||
"@scalar/api-client-react/@scalar/types": ["@scalar/types@0.2.3", "", { "dependencies": { "@scalar/openapi-types": "0.3.3", "nanoid": "^5.1.5", "zod": "3.24.1" } }, "sha512-K1/Vp5xaQ8TEGDYwHg88OBuQWJRWrE3ToZ0Z2LJwskcY1Eg+uDiRQXKF4QPngZzu5rrWgRErAq4yte4Y7omqgA=="],
|
||||
|
||||
"@scalar/components/@scalar/oas-utils": ["@scalar/oas-utils@0.4.7", "", { "dependencies": { "@hyperjump/browser": "^1.1.0", "@hyperjump/json-schema": "^1.9.6", "@scalar/helpers": "0.0.4", "@scalar/object-utils": "1.2.1", "@scalar/openapi-types": "0.3.3", "@scalar/themes": "0.13.4", "@scalar/types": "0.2.3", "@types/har-format": "^1.2.15", "flatted": "^3.3.1", "microdiff": "^1.4.0", "nanoid": "^5.1.5", "type-fest": "^4.20.0", "yaml": "^2.4.5", "zod": "3.24.1" } }, "sha512-Vp8iZZCjNKXcPpL0Yr6dmy+VOSN1LZ5CdlGAHtp+pxuQTeahV6FxiVjMc9kdFve77fTNyut4seVAD6d7rDUDcA=="],
|
||||
|
||||
"@scalar/components/@scalar/themes": ["@scalar/themes@0.13.4", "", { "dependencies": { "@scalar/types": "0.2.3", "nanoid": "^5.1.5" } }, "sha512-PMiSqX+MZF29u32ogEjTVA5c/LBd5cE/QPF7lK0EJeemXRy2r/nWbvrXtqD9Y7Sw54g1caVNjK8yJcCYJdB6hw=="],
|
||||
@@ -3983,7 +3990,7 @@
|
||||
|
||||
"@scalar/themes/@scalar/types": ["@scalar/types@0.1.7", "", { "dependencies": { "@scalar/openapi-types": "0.2.0", "@unhead/schema": "^1.11.11", "nanoid": "^5.1.5", "type-fest": "^4.20.0", "zod": "^3.23.8" } }, "sha512-irIDYzTQG2KLvFbuTI8k2Pz/R4JR+zUUSykVTbEMatkzMmVFnn1VzNSMlODbadycwZunbnL2tA27AXed9URVjw=="],
|
||||
|
||||
"@scalar/types/@scalar/openapi-types": ["@scalar/openapi-types@0.3.3", "", { "dependencies": { "zod": "3.24.1" } }, "sha512-mr3OvbCyvuzSDgv9gR9D3pJAsl82BRNBp1P8ldgRgKF4Wiswfa7nQeKibpX0kHSGXLgiyvxWJULUFwgdWMXNNA=="],
|
||||
"@scalar/types/@scalar/openapi-types": ["@scalar/openapi-types@0.2.3", "", { "dependencies": { "zod": "3.24.1" } }, "sha512-O1GwqLpcRc3GKXTbeBZ5E12fXR2ltpqGWk4RfhoN4ebKZsPVknV5at5425G97E1SwMy12BporRvn90k1Z+MruQ=="],
|
||||
|
||||
"@scalar/types/zod": ["zod@3.24.1", "", {}, "sha512-muH7gBL9sI1nciMZV67X5fTKKBLtwpZ5VBp1vsOQzj1MhrBZ4wlVCm3gedKZWLp0Oyel8sIGfeiz54Su+OVT+A=="],
|
||||
|
||||
@@ -5081,18 +5088,28 @@
|
||||
|
||||
"@radix-ui/react-visually-hidden/@radix-ui/react-primitive/@radix-ui/react-slot": ["@radix-ui/react-slot@1.1.1", "", { "dependencies": { "@radix-ui/react-compose-refs": "1.1.1" }, "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-RApLLOcINYJA+dMVbOju7MYv1Mb2EBp2nH4HdDzXTSyaR5optlm6Otrz1euW3HbdOR8UmmFK06TD+A9frYWv+g=="],
|
||||
|
||||
"@scalar/api-client-react/@scalar/types/@scalar/openapi-types": ["@scalar/openapi-types@0.3.3", "", { "dependencies": { "zod": "3.24.1" } }, "sha512-mr3OvbCyvuzSDgv9gR9D3pJAsl82BRNBp1P8ldgRgKF4Wiswfa7nQeKibpX0kHSGXLgiyvxWJULUFwgdWMXNNA=="],
|
||||
|
||||
"@scalar/api-client-react/@scalar/types/zod": ["zod@3.24.1", "", {}, "sha512-muH7gBL9sI1nciMZV67X5fTKKBLtwpZ5VBp1vsOQzj1MhrBZ4wlVCm3gedKZWLp0Oyel8sIGfeiz54Su+OVT+A=="],
|
||||
|
||||
"@scalar/api-client/pretty-ms/parse-ms": ["parse-ms@3.0.0", "", {}, "sha512-Tpb8Z7r7XbbtBTrM9UhpkzzaMrqA2VXMT3YChzYltwV3P3pM6t8wl7TvpMnSTosz1aQAdVib7kdoys7vYOPerw=="],
|
||||
|
||||
"@scalar/components/@scalar/oas-utils/@scalar/object-utils": ["@scalar/object-utils@1.2.1", "", { "dependencies": { "flatted": "^3.3.1", "just-clone": "^6.2.0", "ts-deepmerge": "^7.0.1", "type-fest": "^4.20.0" } }, "sha512-2P/0In6XSrV1Ye8yU3LyOWJKWkzXpxf0thHmTfJriqGBZz41s06td2KKqoqHSEEi+FAsl3O1ngmOOzLM3QUkkw=="],
|
||||
|
||||
"@scalar/components/@scalar/oas-utils/@scalar/openapi-types": ["@scalar/openapi-types@0.3.3", "", { "dependencies": { "zod": "3.24.1" } }, "sha512-mr3OvbCyvuzSDgv9gR9D3pJAsl82BRNBp1P8ldgRgKF4Wiswfa7nQeKibpX0kHSGXLgiyvxWJULUFwgdWMXNNA=="],
|
||||
|
||||
"@scalar/components/@scalar/oas-utils/@scalar/types": ["@scalar/types@0.2.3", "", { "dependencies": { "@scalar/openapi-types": "0.3.3", "nanoid": "^5.1.5", "zod": "3.24.1" } }, "sha512-K1/Vp5xaQ8TEGDYwHg88OBuQWJRWrE3ToZ0Z2LJwskcY1Eg+uDiRQXKF4QPngZzu5rrWgRErAq4yte4Y7omqgA=="],
|
||||
|
||||
"@scalar/components/@scalar/oas-utils/zod": ["zod@3.24.1", "", {}, "sha512-muH7gBL9sI1nciMZV67X5fTKKBLtwpZ5VBp1vsOQzj1MhrBZ4wlVCm3gedKZWLp0Oyel8sIGfeiz54Su+OVT+A=="],
|
||||
|
||||
"@scalar/components/@scalar/themes/@scalar/types": ["@scalar/types@0.2.3", "", { "dependencies": { "@scalar/openapi-types": "0.3.3", "nanoid": "^5.1.5", "zod": "3.24.1" } }, "sha512-K1/Vp5xaQ8TEGDYwHg88OBuQWJRWrE3ToZ0Z2LJwskcY1Eg+uDiRQXKF4QPngZzu5rrWgRErAq4yte4Y7omqgA=="],
|
||||
|
||||
"@scalar/postman-to-openapi/@scalar/oas-utils/@scalar/object-utils": ["@scalar/object-utils@1.2.1", "", { "dependencies": { "flatted": "^3.3.1", "just-clone": "^6.2.0", "ts-deepmerge": "^7.0.1", "type-fest": "^4.20.0" } }, "sha512-2P/0In6XSrV1Ye8yU3LyOWJKWkzXpxf0thHmTfJriqGBZz41s06td2KKqoqHSEEi+FAsl3O1ngmOOzLM3QUkkw=="],
|
||||
|
||||
"@scalar/postman-to-openapi/@scalar/oas-utils/@scalar/themes": ["@scalar/themes@0.13.4", "", { "dependencies": { "@scalar/types": "0.2.3", "nanoid": "^5.1.5" } }, "sha512-PMiSqX+MZF29u32ogEjTVA5c/LBd5cE/QPF7lK0EJeemXRy2r/nWbvrXtqD9Y7Sw54g1caVNjK8yJcCYJdB6hw=="],
|
||||
|
||||
"@scalar/postman-to-openapi/@scalar/oas-utils/@scalar/types": ["@scalar/types@0.2.3", "", { "dependencies": { "@scalar/openapi-types": "0.3.3", "nanoid": "^5.1.5", "zod": "3.24.1" } }, "sha512-K1/Vp5xaQ8TEGDYwHg88OBuQWJRWrE3ToZ0Z2LJwskcY1Eg+uDiRQXKF4QPngZzu5rrWgRErAq4yte4Y7omqgA=="],
|
||||
|
||||
"@scalar/postman-to-openapi/@scalar/oas-utils/zod": ["zod@3.24.1", "", {}, "sha512-muH7gBL9sI1nciMZV67X5fTKKBLtwpZ5VBp1vsOQzj1MhrBZ4wlVCm3gedKZWLp0Oyel8sIGfeiz54Su+OVT+A=="],
|
||||
|
||||
"@scalar/postman-to-openapi/@scalar/openapi-types/zod": ["zod@3.24.1", "", {}, "sha512-muH7gBL9sI1nciMZV67X5fTKKBLtwpZ5VBp1vsOQzj1MhrBZ4wlVCm3gedKZWLp0Oyel8sIGfeiz54Su+OVT+A=="],
|
||||
@@ -5439,6 +5456,10 @@
|
||||
|
||||
"@radix-ui/react-visually-hidden/@radix-ui/react-primitive/@radix-ui/react-slot/@radix-ui/react-compose-refs": ["@radix-ui/react-compose-refs@1.1.1", "", { "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-Y9VzoRDSJtgFMUCoiZBDVo084VQ5hfpXxVE+NgkdNsjiDBByiImMZKKhxMwCbdHvhlENG6a833CbFkOQvTricw=="],
|
||||
|
||||
"@scalar/components/@scalar/themes/@scalar/types/@scalar/openapi-types": ["@scalar/openapi-types@0.3.3", "", { "dependencies": { "zod": "3.24.1" } }, "sha512-mr3OvbCyvuzSDgv9gR9D3pJAsl82BRNBp1P8ldgRgKF4Wiswfa7nQeKibpX0kHSGXLgiyvxWJULUFwgdWMXNNA=="],
|
||||
|
||||
"@scalar/components/@scalar/themes/@scalar/types/zod": ["zod@3.24.1", "", {}, "sha512-muH7gBL9sI1nciMZV67X5fTKKBLtwpZ5VBp1vsOQzj1MhrBZ4wlVCm3gedKZWLp0Oyel8sIGfeiz54Su+OVT+A=="],
|
||||
|
||||
"@smithy/core/@smithy/util-stream/@smithy/fetch-http-handler/@smithy/querystring-builder": ["@smithy/querystring-builder@4.0.1", "", { "dependencies": { "@smithy/types": "^4.1.0", "@smithy/util-uri-escape": "^4.0.0", "tslib": "^2.6.2" } }, "sha512-wU87iWZoCbcqrwszsOewEIuq+SU2mSoBE2CcsLwE0I19m0B2gOJr1MVjxWcDQYOzHbR1xCk7AcOBbGFUYOKvdg=="],
|
||||
|
||||
"@smithy/core/@smithy/util-stream/@smithy/node-http-handler/@smithy/abort-controller": ["@smithy/abort-controller@4.0.1", "", { "dependencies": { "@smithy/types": "^4.1.0", "tslib": "^2.6.2" } }, "sha512-fiUIYgIgRjMWznk6iLJz35K2YxSLHzLBA/RC6lBrKfQ8fHbPfvk7Pk9UvpKoHgJjI18MnbPuEju53zcVy6KF1g=="],
|
||||
|
||||
@@ -7,6 +7,7 @@
|
||||
"@gitbook/browser-types": "workspace:*",
|
||||
"@gitbook/cache-tags": "workspace:*",
|
||||
"@gitbook/colors": "workspace:*",
|
||||
"@gitbook/expr": "workspace:*",
|
||||
"@gitbook/emoji-codepoints": "workspace:*",
|
||||
"@gitbook/fonts": "workspace:*",
|
||||
"@gitbook/icons": "workspace:*",
|
||||
|
||||
@@ -0,0 +1,103 @@
|
||||
'use client';
|
||||
|
||||
import type { GitBookSiteContext } from '@/lib/context';
|
||||
import { OpenAPIPrefillContextProvider } from '@gitbook/react-openapi';
|
||||
import * as React from 'react';
|
||||
import { createContext, useContext } from 'react';
|
||||
|
||||
export type AdaptiveVisitorClaimsData = {
|
||||
visitor: {
|
||||
claims: Record<string, unknown> & { unsigned: Record<string, unknown> };
|
||||
};
|
||||
};
|
||||
|
||||
/**
|
||||
* In-memory cache of visitor claim readers keyed by contextId.
|
||||
*/
|
||||
const adaptiveVisitorReaderCache = new Map<
|
||||
string,
|
||||
ReturnType<typeof createResourceReader<AdaptiveVisitorClaimsData | null>>
|
||||
>();
|
||||
|
||||
function createResourceReader<T>(promise: Promise<T>) {
|
||||
let result: T | null | undefined;
|
||||
|
||||
const suspender = (async () => {
|
||||
try {
|
||||
result = await promise;
|
||||
} catch {
|
||||
result = null;
|
||||
}
|
||||
})();
|
||||
|
||||
return {
|
||||
read() {
|
||||
if (result === undefined) {
|
||||
throw suspender;
|
||||
}
|
||||
return result;
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Return an adaptive visitor claims cached reader for a given endpoint URL and contextId.
|
||||
*/
|
||||
function getAdaptiveVisitorClaimsReader(url: string, contextId: string) {
|
||||
let reader = adaptiveVisitorReaderCache.get(contextId);
|
||||
if (!reader) {
|
||||
const promise = (async () => {
|
||||
try {
|
||||
const res = await fetch(url);
|
||||
if (!res.ok) {
|
||||
return null;
|
||||
}
|
||||
return await res.json<AdaptiveVisitorClaimsData>();
|
||||
} catch {
|
||||
return null;
|
||||
}
|
||||
})();
|
||||
|
||||
reader = createResourceReader(promise);
|
||||
adaptiveVisitorReaderCache.set(contextId, reader);
|
||||
}
|
||||
return reader;
|
||||
}
|
||||
|
||||
export type AdaptiveVisitorContextValue = () => AdaptiveVisitorClaimsData | null;
|
||||
|
||||
const AdaptiveVisitorContext = createContext<AdaptiveVisitorContextValue>(() => null);
|
||||
|
||||
/**
|
||||
* Provide context to adapt site based on visitor claims.
|
||||
*/
|
||||
export function AdaptiveVisitorContextProvider(
|
||||
props: React.PropsWithChildren<{
|
||||
visitorClaimsURL: string;
|
||||
contextId: GitBookSiteContext['contextId'] | undefined;
|
||||
}>
|
||||
) {
|
||||
const { visitorClaimsURL, contextId, children } = props;
|
||||
|
||||
const getAdaptiveVisitorClaims = React.useCallback(() => {
|
||||
if (!contextId) {
|
||||
return null;
|
||||
}
|
||||
return getAdaptiveVisitorClaimsReader(visitorClaimsURL, contextId).read();
|
||||
}, [visitorClaimsURL, contextId]);
|
||||
|
||||
return (
|
||||
<AdaptiveVisitorContext.Provider value={getAdaptiveVisitorClaims}>
|
||||
<OpenAPIPrefillContextProvider getPrefillInputContextData={getAdaptiveVisitorClaims}>
|
||||
{children}
|
||||
</OpenAPIPrefillContextProvider>
|
||||
</AdaptiveVisitorContext.Provider>
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Hook that returns a suspensable getter for adaptive visitor claims data.
|
||||
*/
|
||||
export function useAdaptiveVisitor(): AdaptiveVisitorContextValue {
|
||||
return useContext(AdaptiveVisitorContext);
|
||||
}
|
||||
@@ -0,0 +1 @@
|
||||
export * from './AdaptiveVisitorContextProvider';
|
||||
@@ -17,6 +17,7 @@ import { GITBOOK_APP_URL } from '@/lib/env';
|
||||
import { AIChatProvider } from '../AI';
|
||||
import type { RenderAIMessageOptions } from '../AI';
|
||||
import { AIChat } from '../AIChat';
|
||||
import { AdaptiveVisitorContextProvider } from '../Adaptive';
|
||||
import { Announcement } from '../Announcement';
|
||||
import { SpacesDropdown } from '../Header/SpacesDropdown';
|
||||
import { InsightsProvider } from '../Insights';
|
||||
@@ -56,29 +57,38 @@ export function SpaceLayoutServerContext(props: SpaceLayoutProps) {
|
||||
eventUrl.searchParams.set('o', context.organizationId);
|
||||
eventUrl.searchParams.set('s', context.site.id);
|
||||
|
||||
const getVisitorClaimsUrl = context.linker.toAbsoluteURL(
|
||||
context.linker.toPathInSite('/~gitbook/visitor')
|
||||
);
|
||||
|
||||
return (
|
||||
<SpaceLayoutContextProvider basePath={context.linker.toPathInSpace('')}>
|
||||
<CurrentContentProvider
|
||||
organizationId={context.organizationId}
|
||||
siteId={context.site.id}
|
||||
siteSectionId={context.sections?.current?.id ?? null}
|
||||
siteSpaceId={context.siteSpace.id}
|
||||
siteShareKey={context.shareKey ?? null}
|
||||
spaceId={context.space.id}
|
||||
revisionId={context.revisionId}
|
||||
visitorAuthClaims={visitorAuthClaims}
|
||||
<AdaptiveVisitorContextProvider
|
||||
contextId={context.contextId}
|
||||
visitorClaimsURL={getVisitorClaimsUrl}
|
||||
>
|
||||
<InsightsProvider
|
||||
enabled={withTracking}
|
||||
appURL={GITBOOK_APP_URL}
|
||||
eventUrl={eventUrl.toString()}
|
||||
visitorCookieTrackingEnabled={customization.insights?.trackingCookie}
|
||||
<CurrentContentProvider
|
||||
organizationId={context.organizationId}
|
||||
siteId={context.site.id}
|
||||
siteSectionId={context.sections?.current?.id ?? null}
|
||||
siteSpaceId={context.siteSpace.id}
|
||||
siteShareKey={context.shareKey ?? null}
|
||||
spaceId={context.space.id}
|
||||
revisionId={context.revisionId}
|
||||
visitorAuthClaims={visitorAuthClaims}
|
||||
>
|
||||
<AIChatProvider renderMessageOptions={aiChatRenderMessageOptions}>
|
||||
{children}
|
||||
</AIChatProvider>
|
||||
</InsightsProvider>
|
||||
</CurrentContentProvider>
|
||||
<InsightsProvider
|
||||
enabled={withTracking}
|
||||
appURL={GITBOOK_APP_URL}
|
||||
eventUrl={eventUrl.toString()}
|
||||
visitorCookieTrackingEnabled={customization.insights?.trackingCookie}
|
||||
>
|
||||
<AIChatProvider renderMessageOptions={aiChatRenderMessageOptions}>
|
||||
{children}
|
||||
</AIChatProvider>
|
||||
</InsightsProvider>
|
||||
</CurrentContentProvider>
|
||||
</AdaptiveVisitorContextProvider>
|
||||
</SpaceLayoutContextProvider>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { type JwtPayload, jwtDecode } from 'jwt-decode';
|
||||
import type { NextRequest } from 'next/server';
|
||||
import { type NextRequest, NextResponse } from 'next/server';
|
||||
import hash from 'object-hash';
|
||||
|
||||
const VISITOR_AUTH_PARAM = 'jwt_token';
|
||||
@@ -443,3 +443,45 @@ export function getVisitorAuthCookieMaxAge(decoded: JwtPayload): number {
|
||||
|
||||
return defaultMaxAge;
|
||||
}
|
||||
|
||||
/**
|
||||
* Handler for the /~gitbook/visitor middleware route to expose visitor data.
|
||||
*/
|
||||
export function serveVisitorClaimsDataRequest(request: NextRequest, siteRequestURL: URL) {
|
||||
const { visitorToken, unsignedClaims } = getVisitorData({
|
||||
cookies: request.cookies.getAll(),
|
||||
url: siteRequestURL,
|
||||
});
|
||||
|
||||
if (!visitorToken && !Object.keys(unsignedClaims).length) {
|
||||
return NextResponse.json({});
|
||||
}
|
||||
|
||||
const visitorClaims = {
|
||||
visitor: {
|
||||
claims: {
|
||||
unsigned: unsignedClaims,
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
if (!visitorToken) {
|
||||
return NextResponse.json(visitorClaims);
|
||||
}
|
||||
|
||||
try {
|
||||
const decodedJwtPayload = jwtDecode(visitorToken.token);
|
||||
return NextResponse.json({
|
||||
visitor: {
|
||||
claims: {
|
||||
...visitorClaims.visitor.claims,
|
||||
...decodedJwtPayload,
|
||||
},
|
||||
},
|
||||
});
|
||||
} catch (error) {
|
||||
console.warn('Error decoding visitor token', error);
|
||||
}
|
||||
|
||||
return NextResponse.json(visitorClaims);
|
||||
}
|
||||
|
||||
@@ -22,6 +22,7 @@ import {
|
||||
getResponseCookiesForVisitorAuth,
|
||||
getVisitorData,
|
||||
normalizeVisitorURL,
|
||||
serveVisitorClaimsDataRequest,
|
||||
} from '@/lib/visitors';
|
||||
import { serveResizedImage } from '@/routes/image';
|
||||
import { cookies } from 'next/headers';
|
||||
@@ -126,7 +127,6 @@ async function serveSiteRoutes(requestURL: URL, request: NextRequest) {
|
||||
|
||||
const { url: siteRequestURL, mode } = match;
|
||||
const imagesContextId = getImageResizingContextId(siteRequestURL);
|
||||
|
||||
/**
|
||||
* Serve image resizing requests (all requests containing `/~gitbook/image`).
|
||||
* All URLs containing `/~gitbook/image` are rewritten to `/~gitbook/image`
|
||||
@@ -154,6 +154,11 @@ async function serveSiteRoutes(requestURL: URL, request: NextRequest) {
|
||||
});
|
||||
}
|
||||
|
||||
// Handler that returns visitor data for the app to consume.
|
||||
if (siteRequestURL.pathname.endsWith('/~gitbook/visitor')) {
|
||||
return serveVisitorClaimsDataRequest(request, siteRequestURL);
|
||||
}
|
||||
|
||||
//
|
||||
// Detect and extract the visitor authentication token from the request
|
||||
//
|
||||
|
||||
@@ -70,6 +70,13 @@ export interface OpenAPICustomOperationProperties {
|
||||
'x-stability'?: OpenAPIStability;
|
||||
}
|
||||
|
||||
/**
|
||||
* Custom properties that can be defined to enable prefilling for OpenAPI blocks (e.g TryIt functionality).
|
||||
*/
|
||||
export interface OpenAPICustomPrefillProperties {
|
||||
'x-gitbook-prefill'?: string;
|
||||
}
|
||||
|
||||
export type OpenAPIStability = 'experimental' | 'alpha' | 'beta';
|
||||
|
||||
/**
|
||||
|
||||
@@ -11,9 +11,11 @@
|
||||
"version": "1.3.6",
|
||||
"sideEffects": false,
|
||||
"dependencies": {
|
||||
"@gitbook/expr": "workspace:*",
|
||||
"@gitbook/openapi-parser": "workspace:*",
|
||||
"@scalar/api-client-react": "^1.3.16",
|
||||
"@scalar/oas-utils": "^0.2.130",
|
||||
"@scalar/types": "^0.1.9",
|
||||
"clsx": "^2.1.1",
|
||||
"flatted": "^3.2.9",
|
||||
"json-xml-parse": "^1.3.0",
|
||||
|
||||
@@ -194,7 +194,7 @@ function OpenAPICodeSampleFooter(props: {
|
||||
context: OpenAPIContext;
|
||||
}) {
|
||||
const { data, context, renderers } = props;
|
||||
const { method, path } = data;
|
||||
const { method, path, securities, servers } = data;
|
||||
const { specUrl } = context;
|
||||
const hideTryItPanel = data['x-hideTryItPanel'] || data.operation['x-hideTryItPanel'];
|
||||
const hasMultipleMediaTypes =
|
||||
@@ -226,6 +226,8 @@ function OpenAPICodeSampleFooter(props: {
|
||||
context={getOpenAPIClientContext(context)}
|
||||
method={method}
|
||||
path={path}
|
||||
securities={securities}
|
||||
servers={servers}
|
||||
specUrl={specUrl}
|
||||
/>
|
||||
)}
|
||||
|
||||
@@ -0,0 +1,40 @@
|
||||
'use client';
|
||||
|
||||
import * as React from 'react';
|
||||
|
||||
/**
|
||||
* Prefill data that can be used to dynamically inject info into OpenAPI operation blocks.
|
||||
*
|
||||
* This is typically dynamic input context, such as visitor data or environment info.
|
||||
*/
|
||||
export type PrefillInputContextData = Record<string, unknown>;
|
||||
|
||||
/**
|
||||
* Context value is function that returns prefill data.
|
||||
*/
|
||||
type PrefillContextValue = () => PrefillInputContextData | null;
|
||||
|
||||
const OpenAPIPrefillContext = React.createContext<PrefillContextValue | null>(null);
|
||||
|
||||
/**
|
||||
* Provide context to help prefill dynamic info like visitor data in OpenAPI blocks.
|
||||
*/
|
||||
export function OpenAPIPrefillContextProvider(
|
||||
props: React.PropsWithChildren<{
|
||||
getPrefillInputContextData: () => PrefillInputContextData | null;
|
||||
}>
|
||||
) {
|
||||
const { getPrefillInputContextData, children } = props;
|
||||
return (
|
||||
<OpenAPIPrefillContext.Provider value={getPrefillInputContextData}>
|
||||
{children}
|
||||
</OpenAPIPrefillContext.Provider>
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Hook to access the prefill context function.
|
||||
*/
|
||||
export function useOpenAPIPrefillContext(): PrefillContextValue {
|
||||
return React.useContext(OpenAPIPrefillContext) ?? (() => null);
|
||||
}
|
||||
@@ -1,13 +1,16 @@
|
||||
'use client';
|
||||
|
||||
import { ApiClientModalProvider, useApiClientModal } from '@scalar/api-client-react';
|
||||
import { useEffect, useImperativeHandle, useMemo, useRef, useState } from 'react';
|
||||
import { Suspense, useEffect, useImperativeHandle, useMemo, useRef, useState } from 'react';
|
||||
import { createPortal } from 'react-dom';
|
||||
|
||||
import type { OpenAPIV3_1 } from '@gitbook/openapi-parser';
|
||||
import { useOpenAPIOperationContext } from './OpenAPIOperationContext';
|
||||
import { useOpenAPIPrefillContext } from './OpenAPIPrefillContextProvider';
|
||||
import type { OpenAPIClientContext } from './context';
|
||||
import { t } from './translate';
|
||||
import type { OpenAPIOperationData } from './types';
|
||||
import { resolveTryItPrefillForOperation } from './util/tryit-prefill';
|
||||
|
||||
/**
|
||||
* Button which launches the Scalar API Client
|
||||
@@ -15,12 +18,15 @@ import { t } from './translate';
|
||||
export function ScalarApiButton(props: {
|
||||
method: OpenAPIV3_1.HttpMethods;
|
||||
path: string;
|
||||
securities: OpenAPIOperationData['securities'];
|
||||
servers: OpenAPIOperationData['servers'];
|
||||
specUrl: string;
|
||||
context: OpenAPIClientContext;
|
||||
}) {
|
||||
const { method, path, specUrl, context } = props;
|
||||
const { method, path, securities, servers, specUrl, context } = props;
|
||||
const [isOpen, setIsOpen] = useState(false);
|
||||
const controllerRef = useRef<ScalarModalControllerRef>(null);
|
||||
|
||||
return (
|
||||
<div className="scalar scalar-activate">
|
||||
<button
|
||||
@@ -42,12 +48,16 @@ export function ScalarApiButton(props: {
|
||||
|
||||
{isOpen &&
|
||||
createPortal(
|
||||
<ScalarModal
|
||||
controllerRef={controllerRef}
|
||||
method={method}
|
||||
path={path}
|
||||
specUrl={specUrl}
|
||||
/>,
|
||||
<Suspense fallback={null}>
|
||||
<ScalarModal
|
||||
controllerRef={controllerRef}
|
||||
method={method}
|
||||
path={path}
|
||||
securities={securities}
|
||||
servers={servers}
|
||||
specUrl={specUrl}
|
||||
/>
|
||||
</Suspense>,
|
||||
document.body
|
||||
)}
|
||||
</div>
|
||||
@@ -57,12 +67,26 @@ export function ScalarApiButton(props: {
|
||||
function ScalarModal(props: {
|
||||
method: OpenAPIV3_1.HttpMethods;
|
||||
path: string;
|
||||
securities: OpenAPIOperationData['securities'];
|
||||
servers: OpenAPIOperationData['servers'];
|
||||
specUrl: string;
|
||||
controllerRef: React.Ref<ScalarModalControllerRef>;
|
||||
}) {
|
||||
const { method, path, specUrl, controllerRef } = props;
|
||||
const { method, path, securities, servers, specUrl, controllerRef } = props;
|
||||
|
||||
const getPrefillInputContextData = useOpenAPIPrefillContext();
|
||||
const prefillInputContext = getPrefillInputContextData();
|
||||
|
||||
const prefillConfig = resolveTryItPrefillForOperation({
|
||||
operation: { securities, servers },
|
||||
prefillInputContext,
|
||||
});
|
||||
|
||||
return (
|
||||
<ApiClientModalProvider configuration={{ url: specUrl }} initialRequest={{ method, path }}>
|
||||
<ApiClientModalProvider
|
||||
configuration={{ url: specUrl, ...prefillConfig }}
|
||||
initialRequest={{ method, path }}
|
||||
>
|
||||
<ScalarModalController method={method} path={path} controllerRef={controllerRef} />
|
||||
</ApiClientModalProvider>
|
||||
);
|
||||
@@ -84,7 +108,11 @@ function ScalarModalController(props: {
|
||||
const openClient = useMemo(() => {
|
||||
if (openScalarClient) {
|
||||
return () => {
|
||||
openScalarClient({ method, path, _source: 'gitbook' });
|
||||
openScalarClient({
|
||||
method,
|
||||
path,
|
||||
_source: 'gitbook',
|
||||
});
|
||||
trackClientOpening({ method, path });
|
||||
};
|
||||
}
|
||||
|
||||
@@ -2,6 +2,7 @@ export * from './schemas';
|
||||
export * from './OpenAPIOperation';
|
||||
export * from './OpenAPIWebhook';
|
||||
export * from './OpenAPIOperationContext';
|
||||
export * from './OpenAPIPrefillContextProvider';
|
||||
export * from './resolveOpenAPIOperation';
|
||||
export * from './resolveOpenAPIWebhook';
|
||||
export type { OpenAPIOperationData, OpenAPIWebhookData } from './types';
|
||||
|
||||
@@ -1,17 +1,31 @@
|
||||
import type {
|
||||
OpenAPICustomOperationProperties,
|
||||
OpenAPICustomPrefillProperties,
|
||||
OpenAPICustomSpecProperties,
|
||||
OpenAPIV3,
|
||||
} from '@gitbook/openapi-parser';
|
||||
|
||||
export type OpenAPISecurityWithRequired = OpenAPIV3.SecuritySchemeObject & { required?: boolean };
|
||||
export type OpenAPIServerVariableWithCustomProperties = OpenAPIV3.ServerVariableObject &
|
||||
OpenAPICustomPrefillProperties;
|
||||
|
||||
/**
|
||||
* OpenAPI ServerObject type extended to provide x-gitbook prefill custom properties at the variable level.
|
||||
*/
|
||||
export type OpenAPIServerWithCustomProperties = Omit<OpenAPIV3.ServerObject, 'variables'> & {
|
||||
variables?: {
|
||||
[variable: string]: OpenAPIServerVariableWithCustomProperties;
|
||||
};
|
||||
} & OpenAPICustomPrefillProperties;
|
||||
|
||||
export type OpenAPISecurityWithRequired = OpenAPIV3.SecuritySchemeObject &
|
||||
OpenAPICustomPrefillProperties & { required?: boolean };
|
||||
|
||||
export interface OpenAPIOperationData extends OpenAPICustomSpecProperties {
|
||||
path: string;
|
||||
method: string;
|
||||
|
||||
/** Servers to be used for this operation */
|
||||
servers: OpenAPIV3.ServerObject[];
|
||||
servers: OpenAPIServerWithCustomProperties[];
|
||||
|
||||
/** Spec of the operation */
|
||||
operation: OpenAPIV3.OperationObject<OpenAPICustomOperationProperties>;
|
||||
|
||||
@@ -0,0 +1,311 @@
|
||||
import { describe, expect, it } from 'bun:test';
|
||||
import type { PrefillInputContextData } from '../OpenAPIPrefillContextProvider';
|
||||
import type { OpenAPIOperationData } from '../types';
|
||||
import { resolveTryItPrefillForOperation } from './tryit-prefill';
|
||||
|
||||
describe('resolveTryItPrefillForOperation', () => {
|
||||
describe('prefill authentication info', () => {
|
||||
it('should resolve prefill for bearer token scheme', () => {
|
||||
const operation: OpenAPIOperationData = {
|
||||
path: '/orgs/<orgId>/spaces/<spaceId>',
|
||||
method: 'GET',
|
||||
operation: { summary: 'Get space by ID' },
|
||||
servers: [{ url: 'https://api.gitbook.com/v1/' }],
|
||||
securities: [
|
||||
[
|
||||
'apiTokenScheme',
|
||||
{
|
||||
type: 'http',
|
||||
scheme: 'bearer',
|
||||
'x-gitbook-prefill': '{{ visitor.claims.apiToken }}',
|
||||
},
|
||||
],
|
||||
],
|
||||
};
|
||||
|
||||
const prefillInputContext: PrefillInputContextData = {
|
||||
visitor: { claims: { apiToken: 'gb_api_testToken' } },
|
||||
};
|
||||
|
||||
const result = resolveTryItPrefillForOperation({
|
||||
operation,
|
||||
prefillInputContext,
|
||||
});
|
||||
|
||||
expect(result).toEqual({
|
||||
authentication: {
|
||||
securitySchemes: {
|
||||
apiTokenScheme: { token: 'gb_api_testToken' },
|
||||
},
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
it('should resolve prefill for basic auth scheme', () => {
|
||||
const operation: OpenAPIOperationData = {
|
||||
path: '/orgs/<orgId>/spaces',
|
||||
method: 'PUT',
|
||||
operation: { summary: 'Create space in an org' },
|
||||
servers: [{ url: 'https://api.gitbook.com/v1/' }],
|
||||
securities: [
|
||||
[
|
||||
'basicAuthScheme',
|
||||
{
|
||||
type: 'http',
|
||||
scheme: 'basic',
|
||||
'x-gitbook-prefill': '{{ visitor.claims.basicAuth }}',
|
||||
},
|
||||
],
|
||||
],
|
||||
};
|
||||
|
||||
const prefillInputContext: PrefillInputContextData = {
|
||||
visitor: { claims: { basicAuth: 'testuser:testpassword' } },
|
||||
};
|
||||
|
||||
const result = resolveTryItPrefillForOperation({
|
||||
operation,
|
||||
prefillInputContext,
|
||||
});
|
||||
|
||||
expect(result).toEqual({
|
||||
authentication: {
|
||||
securitySchemes: {
|
||||
basicAuthScheme: { username: 'testuser', password: 'testpassword' },
|
||||
},
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
it('should resolve prefill for apiKey scheme', () => {
|
||||
const operation: OpenAPIOperationData = {
|
||||
path: '/orgs/<orgId>/spaces/<spaceId>',
|
||||
method: 'POST',
|
||||
operation: { summary: 'Update space by ID' },
|
||||
servers: [{ url: 'https://api.gitbook.com/v1/' }],
|
||||
securities: [
|
||||
[
|
||||
'apiKeyHeader',
|
||||
{
|
||||
type: 'apiKey',
|
||||
in: 'header',
|
||||
name: 'X-API-KEY',
|
||||
'x-gitbook-prefill': '{{ visitor.claims.apiKey }}',
|
||||
},
|
||||
],
|
||||
],
|
||||
};
|
||||
|
||||
const prefillInputContext: PrefillInputContextData = {
|
||||
visitor: { claims: { apiKey: 'key-123' } },
|
||||
};
|
||||
|
||||
const result = resolveTryItPrefillForOperation({
|
||||
operation,
|
||||
prefillInputContext,
|
||||
});
|
||||
|
||||
expect(result).toEqual({
|
||||
authentication: {
|
||||
securitySchemes: {
|
||||
apiKeyHeader: { name: 'X-API-KEY', in: 'header', value: 'key-123' },
|
||||
},
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
it('should return empty object if no visitor data matches prefill expression', () => {
|
||||
const operation: OpenAPIOperationData = {
|
||||
path: '/orgs/<orgId>/spaces',
|
||||
method: 'GET',
|
||||
operation: { summary: 'List all spaces in an org' },
|
||||
servers: [{ url: 'https://api.gitbook.com/v1/' }],
|
||||
securities: [
|
||||
[
|
||||
'bearer',
|
||||
{
|
||||
type: 'http',
|
||||
scheme: 'bearer',
|
||||
'x-gitbook-prefill': '{{ visitor.claims.missing }}',
|
||||
},
|
||||
],
|
||||
],
|
||||
};
|
||||
|
||||
const prefillInputContext: PrefillInputContextData = {
|
||||
visitor: { claims: {} },
|
||||
};
|
||||
|
||||
const result = resolveTryItPrefillForOperation({
|
||||
operation,
|
||||
prefillInputContext,
|
||||
});
|
||||
|
||||
expect(result).toEqual({});
|
||||
});
|
||||
});
|
||||
|
||||
describe('prefill server info', () => {
|
||||
it('should prefill server at url level', () => {
|
||||
const operation: OpenAPIOperationData = {
|
||||
path: '/orgs/<orgId>/spaces',
|
||||
method: 'GET',
|
||||
operation: { summary: 'List spaces in an org' },
|
||||
servers: [
|
||||
{
|
||||
url: 'https://api.gitbook.com/v1/',
|
||||
description: 'GitBook API endpoint',
|
||||
'x-gitbook-prefill': '{{ visitor.claims.api.endpointUrl }}',
|
||||
},
|
||||
],
|
||||
securities: [],
|
||||
};
|
||||
|
||||
const prefillInputContext: PrefillInputContextData = {
|
||||
visitor: {
|
||||
claims: { api: { endpointUrl: 'https://api.gitbook-staging.com/v1/' } },
|
||||
},
|
||||
};
|
||||
|
||||
const result = resolveTryItPrefillForOperation({
|
||||
operation,
|
||||
prefillInputContext,
|
||||
});
|
||||
|
||||
expect(result).toEqual({
|
||||
servers: [
|
||||
{
|
||||
url: 'https://api.gitbook-staging.com/v1/',
|
||||
description: 'GitBook API endpoint',
|
||||
},
|
||||
],
|
||||
});
|
||||
});
|
||||
|
||||
it('should prefill server at variables level', () => {
|
||||
const operation: OpenAPIOperationData = {
|
||||
path: '/orgs/<orgId>/spaces',
|
||||
method: 'GET',
|
||||
operation: { summary: 'List spaces in an org' },
|
||||
servers: [
|
||||
{
|
||||
url: 'https://api.{domain}/{version}/',
|
||||
description: 'Versioned API endpoint by environment',
|
||||
variables: {
|
||||
domain: {
|
||||
default: 'gitbook.com',
|
||||
'x-gitbook-prefill':
|
||||
'{{ visitor.claims.api.env === "staging" ? "gitbook-staging.com" : "gitbook.com" }}',
|
||||
},
|
||||
version: {
|
||||
default: 'v1',
|
||||
'x-gitbook-prefill': '{{ visitor.claims.api.version }}',
|
||||
},
|
||||
},
|
||||
},
|
||||
],
|
||||
securities: [],
|
||||
};
|
||||
|
||||
// Override env
|
||||
const overrideEnvResult = resolveTryItPrefillForOperation({
|
||||
operation,
|
||||
prefillInputContext: { visitor: { claims: { api: { env: 'staging' } } } },
|
||||
});
|
||||
expect(overrideEnvResult).toEqual({
|
||||
servers: [
|
||||
{
|
||||
url: 'https://api.{domain}/{version}/',
|
||||
description: 'Versioned API endpoint by environment',
|
||||
variables: {
|
||||
domain: { default: 'gitbook-staging.com' },
|
||||
version: { default: 'v1' },
|
||||
},
|
||||
},
|
||||
],
|
||||
});
|
||||
|
||||
// Override version
|
||||
const overrideVersionResult = resolveTryItPrefillForOperation({
|
||||
operation,
|
||||
prefillInputContext: { visitor: { claims: { api: { version: 'v2' } } } },
|
||||
});
|
||||
expect(overrideVersionResult).toEqual({
|
||||
servers: [
|
||||
{
|
||||
url: 'https://api.{domain}/{version}/',
|
||||
description: 'Versioned API endpoint by environment',
|
||||
variables: {
|
||||
domain: { default: 'gitbook.com' },
|
||||
version: { default: 'v2' },
|
||||
},
|
||||
},
|
||||
],
|
||||
});
|
||||
|
||||
// Override both
|
||||
const overrideBoth = resolveTryItPrefillForOperation({
|
||||
operation,
|
||||
prefillInputContext: {
|
||||
visitor: { claims: { api: { env: 'staging', version: 'v2' } } },
|
||||
},
|
||||
});
|
||||
expect(overrideBoth).toEqual({
|
||||
servers: [
|
||||
{
|
||||
url: 'https://api.{domain}/{version}/',
|
||||
description: 'Versioned API endpoint by environment',
|
||||
variables: {
|
||||
domain: { default: 'gitbook-staging.com' },
|
||||
version: { default: 'v2' },
|
||||
},
|
||||
},
|
||||
],
|
||||
});
|
||||
});
|
||||
|
||||
it('should ignore server prefill when no matching visitor data', () => {
|
||||
const operation: OpenAPIOperationData = {
|
||||
path: '/orgs/<orgId>/spaces',
|
||||
method: 'GET',
|
||||
operation: { summary: 'List spaces in an org' },
|
||||
servers: [
|
||||
{
|
||||
url: 'https://api.{domain}/{version}/',
|
||||
description: 'Versioned API endpoint by environment',
|
||||
variables: {
|
||||
domain: {
|
||||
default: 'gitbook.com',
|
||||
'x-gitbook-prefill':
|
||||
'{{ visitor.claims.api.env === "staging" ? "gitbook-staging.com" : "gitbook.com" }}',
|
||||
},
|
||||
version: {
|
||||
default: 'v1',
|
||||
'x-gitbook-prefill': '{{ visitor.claims.api.version }}',
|
||||
},
|
||||
},
|
||||
},
|
||||
],
|
||||
securities: [],
|
||||
};
|
||||
|
||||
const result = resolveTryItPrefillForOperation({
|
||||
operation,
|
||||
prefillInputContext: { visitor: { claims: { isBetaUser: true } } },
|
||||
});
|
||||
|
||||
expect(result).toEqual({
|
||||
servers: [
|
||||
{
|
||||
url: 'https://api.{domain}/{version}/',
|
||||
description: 'Versioned API endpoint by environment',
|
||||
variables: {
|
||||
domain: { default: 'gitbook.com' },
|
||||
version: { default: 'v1' },
|
||||
},
|
||||
},
|
||||
],
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,156 @@
|
||||
import { ExpressionRuntime, parseTemplate } from '@gitbook/expr';
|
||||
import type { OpenAPIV3 } from '@gitbook/openapi-parser';
|
||||
import type { ApiClientConfiguration } from '@scalar/types';
|
||||
import type { PrefillInputContextData } from '../OpenAPIPrefillContextProvider';
|
||||
import type { OpenAPIOperationData } from '../types';
|
||||
|
||||
export interface TryItPrefillConfiguration {
|
||||
authentication?: ApiClientConfiguration['authentication'];
|
||||
servers?: ApiClientConfiguration['servers'];
|
||||
}
|
||||
|
||||
/**
|
||||
* Resolve the Scalar API client prefill configuration for a given OpenAPI operation.
|
||||
*/
|
||||
export function resolveTryItPrefillForOperation(args: {
|
||||
/**
|
||||
* The parsed OpenAPI operation.
|
||||
*/
|
||||
operation: Pick<OpenAPIOperationData, 'securities' | 'servers'>;
|
||||
/**
|
||||
* Prefill input context data.
|
||||
*/
|
||||
prefillInputContext: PrefillInputContextData | null;
|
||||
}): TryItPrefillConfiguration {
|
||||
const {
|
||||
operation: { securities, servers },
|
||||
prefillInputContext,
|
||||
} = args;
|
||||
|
||||
// Fixed ExpressionRuntime and resolveTryItPrefillExpression function
|
||||
const runtime = new ExpressionRuntime();
|
||||
const resolveTryItPrefillExpression = (expr: string) => {
|
||||
if (!prefillInputContext) return undefined;
|
||||
const parts = parseTemplate(expr);
|
||||
if (!parts.length) return undefined;
|
||||
return runtime.evaluateTemplate(expr, prefillInputContext);
|
||||
};
|
||||
|
||||
const prefillAuth = securities
|
||||
? resolveTryItPrefillAuthForOperationSecurities({
|
||||
securities,
|
||||
resolveTryItPrefillExpression,
|
||||
})
|
||||
: undefined;
|
||||
|
||||
const prefillServers = servers
|
||||
? resolveTryItPrefillServersForOperationServers({ servers, resolveTryItPrefillExpression })
|
||||
: [];
|
||||
|
||||
return {
|
||||
...(prefillAuth ? { authentication: prefillAuth } : {}),
|
||||
...(prefillServers ? { servers: prefillServers } : {}),
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Resolve prefill authentication configuration for the security schemes defined for an operation.
|
||||
*/
|
||||
function resolveTryItPrefillAuthForOperationSecurities(args: {
|
||||
securities: OpenAPIOperationData['securities'];
|
||||
resolveTryItPrefillExpression: (expr: string) => string | undefined;
|
||||
}): ApiClientConfiguration['authentication'] | undefined {
|
||||
const { securities, resolveTryItPrefillExpression } = args;
|
||||
const prefillAuthConfig: ApiClientConfiguration['authentication']['securitySchemes'] = {};
|
||||
|
||||
for (const [schemeName, security] of Object.values(securities)) {
|
||||
const tryitPrefillAuthValue = security['x-gitbook-prefill']
|
||||
? resolveTryItPrefillExpression(security['x-gitbook-prefill'])
|
||||
: undefined;
|
||||
|
||||
if (!tryitPrefillAuthValue) {
|
||||
continue;
|
||||
}
|
||||
|
||||
switch (security.type) {
|
||||
case 'http': {
|
||||
if (security.scheme?.includes('bearer')) {
|
||||
prefillAuthConfig[schemeName] = { token: tryitPrefillAuthValue };
|
||||
} else if (
|
||||
security.scheme?.includes('basic') &&
|
||||
tryitPrefillAuthValue.includes(':')
|
||||
) {
|
||||
const [username, password] = tryitPrefillAuthValue.split(':', 2);
|
||||
prefillAuthConfig[schemeName] = { username, password };
|
||||
}
|
||||
break;
|
||||
}
|
||||
case 'apiKey': {
|
||||
prefillAuthConfig[schemeName] = {
|
||||
name: security.name,
|
||||
in: security.in,
|
||||
value: tryitPrefillAuthValue,
|
||||
};
|
||||
break;
|
||||
}
|
||||
case 'oauth2':
|
||||
case 'openIdConnect': {
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return Object.keys(prefillAuthConfig).length > 0
|
||||
? { securitySchemes: prefillAuthConfig }
|
||||
: undefined;
|
||||
}
|
||||
|
||||
/**
|
||||
* Resolve prefill server configuration for the servers defined for an operation.
|
||||
*/
|
||||
function resolveTryItPrefillServersForOperationServers(args: {
|
||||
servers: OpenAPIOperationData['servers'];
|
||||
resolveTryItPrefillExpression: (expr: string) => string | undefined;
|
||||
}): ApiClientConfiguration['servers'] | undefined {
|
||||
const { servers, resolveTryItPrefillExpression } = args;
|
||||
const resolvedServers: ApiClientConfiguration['servers'] = [];
|
||||
|
||||
for (const server of servers) {
|
||||
// Url-level prefill
|
||||
const tryItPrefillServerUrlExpr = server['x-gitbook-prefill'];
|
||||
const tryItPrefillServerUrlValue = tryItPrefillServerUrlExpr
|
||||
? resolveTryItPrefillExpression(tryItPrefillServerUrlExpr)
|
||||
: undefined;
|
||||
|
||||
const variables: { [variable: string]: OpenAPIV3.ServerVariableObject } = server.variables
|
||||
? { ...server.variables }
|
||||
: {};
|
||||
|
||||
// Variable-level prefill
|
||||
if (server.variables) {
|
||||
for (const [varName, variable] of Object.entries(server.variables)) {
|
||||
const { 'x-gitbook-prefill': tryItPrefillVarExpr, ...variableProps } = variable;
|
||||
|
||||
const tryItPrefillVarValue = tryItPrefillVarExpr
|
||||
? resolveTryItPrefillExpression(tryItPrefillVarExpr)
|
||||
: undefined;
|
||||
variables[varName] = {
|
||||
...variableProps,
|
||||
...(tryItPrefillVarValue ? { default: String(tryItPrefillVarValue) } : {}),
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
const hasServerVariables = Object.keys(variables).length > 0;
|
||||
if (server.url && (tryItPrefillServerUrlValue || hasServerVariables)) {
|
||||
const resolvedServer: OpenAPIV3.ServerObject = {
|
||||
url: tryItPrefillServerUrlValue ?? server.url,
|
||||
...(server.description ? { description: server.description } : {}),
|
||||
...(hasServerVariables ? { variables } : {}),
|
||||
};
|
||||
resolvedServers.push(resolvedServer);
|
||||
}
|
||||
}
|
||||
|
||||
return resolvedServers.length > 0 ? resolvedServers : undefined;
|
||||
}
|
||||
Reference in New Issue
Block a user