Compare commits

...

2 Commits

Author SHA1 Message Date
Peter White 384cd63729 changeset 2026-10-14 10:18:25 +02:00
Peter White 0898723168 Expose preview segment claims to expressions 2026-10-14 10:18:25 +02:00
4 changed files with 64 additions and 5 deletions
@@ -0,0 +1,5 @@
---
"gitbook": patch
---
Show expression values in site previews that use a visitor segment.
+26
View File
@@ -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 } },
});
});
});
+16 -2
View File
@@ -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,
+17 -3
View File
@@ -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