mirror of
https://github.com/GitbookIO/gitbook.git
synced 2026-10-14 17:19:42 +00:00
Expose preview segment claims to expressions
This commit is contained in:
@@ -1,5 +1,6 @@
|
||||
import { describe, expect, it } from 'bun:test';
|
||||
import type { JwtPayload } from 'jwt-decode';
|
||||
import { NextRequest } from 'next/server';
|
||||
|
||||
import {
|
||||
type ResponseCookies,
|
||||
@@ -14,6 +15,7 @@ import {
|
||||
isRevalidationRequest,
|
||||
normalizeVisitorURL,
|
||||
parseHostnameList,
|
||||
serveVisitorClaimsDataRequest,
|
||||
shouldSendVisitorCountry,
|
||||
} from './visitors';
|
||||
|
||||
@@ -691,3 +693,27 @@ describe('parseHostnameList', () => {
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe('serveVisitorClaimsDataRequest', () => {
|
||||
it('should expose the claims of the preview API token', async () => {
|
||||
const siteRequestURL = new URL(
|
||||
'https://sites.gitbook.com/preview/site_foo/~gitbook/visitor'
|
||||
);
|
||||
const claims = { apiUrl: 'https://client-123.example.com', isBeta: true };
|
||||
const previewAPIToken = [
|
||||
btoa(JSON.stringify({ alg: 'HS256', typ: 'JWT' })),
|
||||
btoa(JSON.stringify({ kind: 'site', site: 'site_foo', claims })),
|
||||
'signature',
|
||||
].join('.');
|
||||
|
||||
const response = serveVisitorClaimsDataRequest(
|
||||
new NextRequest(siteRequestURL),
|
||||
siteRequestURL,
|
||||
previewAPIToken
|
||||
);
|
||||
|
||||
expect(await response.json()).toEqual({
|
||||
visitor: { claims: { unsigned: {}, ...claims } },
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -3,7 +3,7 @@ import { type JwtPayload, jwtDecode } from 'jwt-decode';
|
||||
import { type NextRequest, NextResponse } from 'next/server';
|
||||
import hash from 'object-hash';
|
||||
|
||||
import type { SiteVisitorPayload } from '@gitbook/api';
|
||||
import type { SiteAPIToken, SiteVisitorPayload } from '@gitbook/api';
|
||||
|
||||
import {
|
||||
MAX_CHUNKED_COOKIE_LENGTH,
|
||||
@@ -571,7 +571,21 @@ export function getVisitorAuthCookieMaxAge(decoded: JwtPayload): number {
|
||||
/**
|
||||
* Handler for the /~gitbook/visitor middleware route to expose visitor data.
|
||||
*/
|
||||
export function serveVisitorClaimsDataRequest(request: NextRequest, siteRequestURL: URL) {
|
||||
export function serveVisitorClaimsDataRequest(
|
||||
request: NextRequest,
|
||||
siteRequestURL: URL,
|
||||
previewAPIToken?: string
|
||||
) {
|
||||
// A site preview carries the visitor segment claims in its API token, not in a visitor token.
|
||||
if (previewAPIToken) {
|
||||
try {
|
||||
const { claims } = jwtDecode<SiteAPIToken>(previewAPIToken);
|
||||
return NextResponse.json({ visitor: { claims: { unsigned: {}, ...claims } } });
|
||||
} catch (error) {
|
||||
console.warn('Error decoding preview API token', error);
|
||||
}
|
||||
}
|
||||
|
||||
const { visitorToken, unsignedClaims } = getVisitorData({
|
||||
cookies: request.cookies.getAll(),
|
||||
headers: request.headers,
|
||||
|
||||
@@ -210,7 +210,13 @@ 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);
|
||||
const previewAPIToken = isPreviewRequest(siteRequestURL)
|
||||
? getAPITokenFromCookies(
|
||||
request.cookies.getAll(),
|
||||
getAPITokenCookieName(getPreviewScopePath(siteRequestURL))
|
||||
)
|
||||
: undefined;
|
||||
return serveVisitorClaimsDataRequest(request, siteRequestURL, previewAPIToken);
|
||||
}
|
||||
|
||||
//
|
||||
@@ -625,7 +631,7 @@ async function serveSiteRoutes(requestURL: URL, request: NextRequest) {
|
||||
request.headers.set('x-gitbook-disable-tracking', 'true');
|
||||
return serveWithQueryAPIToken({
|
||||
// We scope the API token to the site ID.
|
||||
scopePath: ['preview', getPreviewRequestIdentifier(siteRequestURL)].join('/'),
|
||||
scopePath: getPreviewScopePath(siteRequestURL),
|
||||
// We keep the original request URL when using `url` mode
|
||||
requestURL: mode === 'url' ? requestURL : siteRequestURL,
|
||||
requestCookies: request.cookies,
|
||||
@@ -677,6 +683,14 @@ function serveErrorResponse(error: Error) {
|
||||
throw error;
|
||||
}
|
||||
|
||||
function getPreviewScopePath(siteRequestURL: URL) {
|
||||
return ['preview', getPreviewRequestIdentifier(siteRequestURL)].join('/');
|
||||
}
|
||||
|
||||
function getAPITokenCookieName(scopePath: string) {
|
||||
return getPathScopedCookieName('gitbook-api-token', scopePath);
|
||||
}
|
||||
|
||||
/**
|
||||
* Serve a response with an API token obtained from the query params.
|
||||
*/
|
||||
@@ -689,7 +703,7 @@ async function serveWithQueryAPIToken(input: {
|
||||
const { scopePath, requestURL, requestCookies, serve } = input;
|
||||
// We store the API token in a cookie that is scoped to the specific route
|
||||
// to avoid errors when multiple previews are opened in different tabs.
|
||||
const cookieName = getPathScopedCookieName('gitbook-api-token', scopePath);
|
||||
const cookieName = getAPITokenCookieName(scopePath);
|
||||
|
||||
// Extract a potential GitBook API token passed in the request
|
||||
// If found, we redirect to the same URL but with the token in the cookie
|
||||
|
||||
Reference in New Issue
Block a user