Skip to content
12 changes: 9 additions & 3 deletions packages/gitbook-v2/src/middleware.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ import {
type ResponseCookies,
getPathScopedCookieName,
getResponseCookiesForVisitorAuth,
getVisitorPayload,
getVisitorData,
normalizeVisitorAuthURL,
} from '@/lib/visitors';
import { serveResizedImage } from '@/routes/image';
Expand Down Expand Up @@ -95,7 +95,7 @@ async function serveSiteRoutes(requestURL: URL, request: NextRequest) {
//
// Detect and extract the visitor authentication token from the request
//
const { visitorToken, unsignedClaims } = getVisitorPayload({
const { visitorToken, unsignedClaims, visitorParamsCookie } = getVisitorData({
cookies: request.cookies.getAll(),
url: siteRequestURL,
});
Expand All @@ -121,7 +121,13 @@ async function serveSiteRoutes(requestURL: URL, request: NextRequest) {
apiToken,
})
);
const cookies: ResponseCookies = [];

const cookies: ResponseCookies = visitorParamsCookie
? [
// If visitor.* params were passed to the site URL, include a session cookie to persist these params across navigation.
visitorParamsCookie,
]
: [];

//
// Handle redirects
Expand Down
42 changes: 34 additions & 8 deletions packages/gitbook/src/lib/visitors.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -178,7 +178,7 @@ describe('getVisitorUnsignedClaims', () => {
const url = new URL('https://example.com/');
const claims = getVisitorUnsignedClaims({ cookies, url });

expect(claims).toStrictEqual({
expect(claims.all).toStrictEqual({
bucket: { flags: { SITE_AI: true, SITE_PREVIEW: true } },
launchdarkly: { flags: { ALPHA: true, API: true } },
});
Expand All @@ -191,7 +191,12 @@ describe('getVisitorUnsignedClaims', () => {

const claims = getVisitorUnsignedClaims({ cookies: [], url });

expect(claims).toStrictEqual({
expect(claims.all).toStrictEqual({
isEnterprise: true,
language: 'fr',
country: 'fr',
});
expect(claims.fromVisitorParams).toStrictEqual({
isEnterprise: true,
language: 'fr',
country: 'fr',
Expand All @@ -203,10 +208,13 @@ describe('getVisitorUnsignedClaims', () => {

const claims = getVisitorUnsignedClaims({ cookies: [], url });

expect(claims).toStrictEqual({
expect(claims.all).toStrictEqual({
isEnterprise: true,
// otherParam is not present
});
expect(claims.fromVisitorParams).toStrictEqual({
isEnterprise: true,
});
});

it('should support nested query param keys via dot notation', () => {
Expand All @@ -216,7 +224,14 @@ describe('getVisitorUnsignedClaims', () => {

const claims = getVisitorUnsignedClaims({ cookies: [], url });

expect(claims).toStrictEqual({
expect(claims.all).toStrictEqual({
isEnterprise: true,
flags: {
ALPHA: true,
API: false,
},
});
expect(claims.fromVisitorParams).toStrictEqual({
isEnterprise: true,
flags: {
ALPHA: true,
Expand All @@ -235,7 +250,7 @@ describe('getVisitorUnsignedClaims', () => {
const url = new URL('https://example.com/');
const claims = getVisitorUnsignedClaims({ cookies, url });

expect(claims).toStrictEqual({});
expect(claims.all).toStrictEqual({});
});

it('should merge claims from cookies and visitor.* query params', () => {
Expand All @@ -250,16 +265,27 @@ describe('getVisitorUnsignedClaims', () => {
},
];
const url = new URL(
'https://example.com/?visitor.isEnterprise=true&visitor.flags.ALPHA=true&visitor.flags.API=false'
'https://example.com/?visitor.isEnterprise=true&visitor.flags.ALPHA=true&visitor.flags.API=false&visitor.bucket.flags.HELLO=false'
);

const claims = getVisitorUnsignedClaims({ cookies, url });

expect(claims).toStrictEqual({
expect(claims.all).toStrictEqual({
role: 'admin',
language: 'fr',
bucket: {
flags: { SITE_AI: true, SITE_PREVIEW: true },
flags: { HELLO: false, SITE_AI: true, SITE_PREVIEW: true },
},
isEnterprise: true,
flags: {
ALPHA: true,
API: false,
},
});

expect(claims.fromVisitorParams).toStrictEqual({
bucket: {
flags: { HELLO: false },
},
isEnterprise: true,
flags: {
Expand Down
55 changes: 47 additions & 8 deletions packages/gitbook/src/lib/visitors.ts
Original file line number Diff line number Diff line change
Expand Up @@ -41,13 +41,15 @@ type ClaimPrimitive =
| ClaimPrimitive[];

/**
* The result of a visitor info lookup that can include:
* The result of a visitor data lookup that can include:
* - a visitor token (JWT)
* - a record of visitor public/unsigned claims (JSON object)
* - a session cookie response to persist any visitor query params across navigations.
*/
export type VisitorPayloadLookup = {
export type VisitorDataLookup = {
visitorToken: VisitorTokenLookup;
unsignedClaims: Record<string, ClaimPrimitive>;
visitorParamsCookie: ResponseCookie | undefined;
};

/**
Expand All @@ -74,21 +76,26 @@ export type VisitorTokenLookup =
| undefined;

/**
* Get the visitor info for the request including its token and/or unsigned claims when present.
* Get the visitor data for the request potentially including:
* - a JWT token that may contain signed claims or can be used for VA authentication.
* - a record of the unsigned claims passed via a cookie or visitor.* params.
* - a session cookie response that is used to persist any visitor.* params that were passed via the site URL.
*/
export function getVisitorPayload({
export function getVisitorData({
cookies,
url,
}: {
cookies: RequestCookies;
url: URL | NextRequest['nextUrl'];
}): VisitorPayloadLookup {
}): VisitorDataLookup {
const visitorToken = getVisitorToken({ cookies, url });
const unsignedClaims = getVisitorUnsignedClaims({ cookies, url });
const visitorParamsCookie = getResponseCookieForVisitorParams(unsignedClaims.fromVisitorParams);

return {
visitorToken,
unsignedClaims,
unsignedClaims: unsignedClaims.all,
visitorParamsCookie,
};
}

Expand Down Expand Up @@ -128,9 +135,19 @@ export function getVisitorToken({
export function getVisitorUnsignedClaims(args: {
cookies: RequestCookies;
url: URL | NextRequest['nextUrl'];
}): Record<string, ClaimPrimitive> {
}): {
/**
* The unsigned claims coming from both `gitbook-visitor-public` cookies and `visitor.*` query params.
*/
all: Record<string, ClaimPrimitive>;
/**
* The unsigned claims from the `visitor.*` query params.
*/
fromVisitorParams: Record<string, ClaimPrimitive>;
} {
const { cookies, url } = args;
const claims: Record<string, ClaimPrimitive> = {};
const searchParamsClaims: Record<string, ClaimPrimitive> = {};

for (const cookie of cookies) {
if (cookie.name.startsWith(VISITOR_UNSIGNED_CLAIMS_PREFIX)) {
Expand All @@ -149,11 +166,13 @@ export function getVisitorUnsignedClaims(args: {
if (key.startsWith('visitor.')) {
const claimPath = key.substring('visitor.'.length);
const claimValue = parseVisitorQueryParamValue(value);

setVisitorClaimByPath(claims, claimPath, claimValue);
setVisitorClaimByPath(searchParamsClaims, claimPath, claimValue);
}
}

return claims;
return { all: claims, fromVisitorParams: searchParamsClaims };
}

/**
Expand Down Expand Up @@ -221,6 +240,26 @@ function parseVisitorQueryParamValue(value: string): ClaimPrimitive {
return value;
}

/**
* Returns to cookie response to use in order to persist visitor params that were passed to the URL.
*/
function getResponseCookieForVisitorParams(
visitorParamsClaims: Record<string, ClaimPrimitive>
): ResponseCookie | undefined {
if (Object.keys(visitorParamsClaims).length === 0) {
return undefined;
}

return {
name: VISITOR_UNSIGNED_CLAIMS_PREFIX,
value: JSON.stringify(visitorParamsClaims),
options: {
sameSite: process.env.NODE_ENV === 'production' ? 'none' : undefined,
secure: process.env.NODE_ENV === 'production',
},
};
}

/**
* Return the lookup result for content served with visitor auth.
*/
Expand Down
6 changes: 3 additions & 3 deletions packages/gitbook/src/middleware.ts
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@ import {
type ResponseCookies,
type VisitorTokenLookup,
getResponseCookiesForVisitorAuth,
getVisitorPayload,
getVisitorData,
normalizeVisitorAuthURL,
} from '@/lib/visitors';

Expand Down Expand Up @@ -392,7 +392,7 @@ async function lookupSiteInProxy(request: NextRequest, url: URL): Promise<Lookup
* When serving multi spaces based on the current URL.
*/
async function lookupSiteInMultiMode(request: NextRequest, url: URL): Promise<LookupResult> {
const { visitorToken } = getVisitorPayload({
const { visitorToken } = getVisitorData({
cookies: request.cookies.getAll(),
url,
});
Expand Down Expand Up @@ -609,7 +609,7 @@ async function lookupSiteInMultiPathMode(request: NextRequest, url: URL): Promis
const target = new URL(targetStr);
target.search = url.search;

const { visitorToken } = getVisitorPayload({
const { visitorToken } = getVisitorData({
cookies: request.cookies.getAll(),
url: target,
});
Expand Down