{
return generateRandomId();
}
- const existingTrackingCookie = cookies.get(VISITORID_COOKIE);
+ const existingTrackingCookie = getBrowserCookie(VISITORID_COOKIE);
if (existingTrackingCookie) {
// If the cookie already exists, we'll just use that. Avoids a server request.
diff --git a/packages/gitbook/src/components/SiteLayout/SiteLayout.tsx b/packages/gitbook/src/components/SiteLayout/SiteLayout.tsx
index 870438a4f..5983b0129 100644
--- a/packages/gitbook/src/components/SiteLayout/SiteLayout.tsx
+++ b/packages/gitbook/src/components/SiteLayout/SiteLayout.tsx
@@ -12,6 +12,7 @@ import { SpaceLayout } from '@/components/SpaceLayout';
import { buildVersion } from '@/lib/build';
import { isSiteIndexable } from '@/lib/seo';
+import type { VisitorAuthClaims } from '@/lib/adaptive';
import { GITBOOK_API_PUBLIC_URL, GITBOOK_ASSETS_URL, GITBOOK_ICONS_URL } from '@v2/lib/env';
import { getResizedImageURL } from '@v2/lib/images';
import { ClientContexts } from './ClientContexts';
@@ -25,10 +26,10 @@ export async function SiteLayout(props: {
context: GitBookSiteContext;
forcedTheme?: CustomizationThemeMode | null;
withTracking: boolean;
- visitorAuthToken: string | null;
+ visitorAuthClaims: VisitorAuthClaims;
children: React.ReactNode;
}) {
- const { context, nonce, forcedTheme, withTracking, visitorAuthToken, children } = props;
+ const { context, nonce, forcedTheme, withTracking, visitorAuthClaims, children } = props;
const { scripts, customization } = context;
@@ -57,7 +58,7 @@ export async function SiteLayout(props: {
{children}
diff --git a/packages/gitbook/src/components/SpaceLayout/SpaceLayout.tsx b/packages/gitbook/src/components/SpaceLayout/SpaceLayout.tsx
index b934f13f4..0baed51ed 100644
--- a/packages/gitbook/src/components/SpaceLayout/SpaceLayout.tsx
+++ b/packages/gitbook/src/components/SpaceLayout/SpaceLayout.tsx
@@ -11,6 +11,7 @@ import { getSpaceLanguage } from '@/intl/server';
import { t } from '@/intl/translate';
import { tcls } from '@/lib/tailwind';
+import type { VisitorAuthClaims } from '@/lib/adaptive';
import { GITBOOK_API_PUBLIC_URL, GITBOOK_APP_URL } from '@v2/lib/env';
import { SpacesDropdown } from '../Header/SpacesDropdown';
import { InsightsProvider } from '../Insights';
@@ -26,13 +27,13 @@ export function SpaceLayout(props: {
/** Whether to enable tracking of events into site insights. */
withTracking: boolean;
- /** The visitor token used to access this content */
- visitorAuthToken: string | null;
+ /** The visitor auth claims. */
+ visitorAuthClaims: VisitorAuthClaims;
/** The children of the layout. */
children: React.ReactNode;
}) {
- const { context, withTracking, visitorAuthToken, children } = props;
+ const { context, withTracking, visitorAuthClaims, children } = props;
const { siteSpace, customization, sections, siteSpaces } = context;
const withTopHeader = customization.header.preset !== CustomizationHeaderPreset.None;
@@ -52,7 +53,6 @@ export function SpaceLayout(props: {
enabled={withTracking}
appURL={GITBOOK_APP_URL}
apiHost={GITBOOK_API_PUBLIC_URL}
- visitorAuthToken={visitorAuthToken}
organizationId={context.organizationId}
siteId={context.site.id}
siteSectionId={context.sections?.current?.id ?? null}
@@ -60,6 +60,7 @@ export function SpaceLayout(props: {
siteShareKey={context.shareKey ?? null}
revisionId={context.revisionId}
spaceId={context.space.id}
+ visitorAuthClaims={visitorAuthClaims}
>
diff --git a/packages/gitbook/src/lib/adaptive.ts b/packages/gitbook/src/lib/adaptive.ts
new file mode 100644
index 000000000..4a389a4f4
--- /dev/null
+++ b/packages/gitbook/src/lib/adaptive.ts
@@ -0,0 +1,23 @@
+import type { PublishedSiteContent, SiteAPIToken } from '@gitbook/api';
+import { jwtDecode } from 'jwt-decode';
+
+/**
+ * Claims about the visitor, stored in the VA and auth token.
+ */
+export type VisitorAuthClaims = Record;
+
+/**
+ * Get the visitor auth claims from the API response obtained from `getPublishedContentByUrl`.
+ */
+export function getVisitorAuthClaims(siteData: PublishedSiteContent): VisitorAuthClaims {
+ const { apiToken } = siteData;
+
+ return getVisitorAuthClaimsFromToken(jwtDecode(apiToken));
+}
+
+/**
+ * Get the visitor auth claims from a decoded API token.
+ */
+export function getVisitorAuthClaimsFromToken(token: SiteAPIToken): VisitorAuthClaims {
+ return token.claims ?? {};
+}
diff --git a/packages/gitbook/src/lib/browser-cookies.ts b/packages/gitbook/src/lib/browser-cookies.ts
new file mode 100644
index 000000000..0a688b763
--- /dev/null
+++ b/packages/gitbook/src/lib/browser-cookies.ts
@@ -0,0 +1,56 @@
+'use client';
+
+import cookies from 'js-cookie';
+
+import { checkIsSecurityError } from './security-error';
+
+/**
+ * Get all cookies.
+ * @returns A map of cookie names to their values.
+ */
+export function getAllBrowserCookies(): Array<{
+ name: string;
+ value: string;
+}> {
+ try {
+ const all = cookies.get();
+ return Object.entries(all).map(([name, value]) => ({ name, value }));
+ } catch (error) {
+ if (checkIsSecurityError(error)) {
+ return [];
+ }
+ throw error;
+ }
+}
+
+/**
+ * Get a map of cookie names to their values.
+ * @returns A map of cookie names to their values.
+ */
+export function getAllBrowserCookiesMap(): Record {
+ return Object.fromEntries(getAllBrowserCookies().map(({ name, value }) => [name, value]));
+}
+
+/**
+ * Get a cookie by name.
+ * @param name - The name of the cookie to get.
+ * @returns The value of the cookie or undefined if the cookie does not exist.
+ */
+export function getBrowserCookie(name: string): string | undefined {
+ try {
+ return cookies.get(name);
+ } catch (error) {
+ if (checkIsSecurityError(error)) {
+ return undefined;
+ }
+ throw error;
+ }
+}
+
+/**
+ * Set a cookie.
+ * @param name - The name of the cookie to set.
+ * @param value - The value of the cookie to set.
+ * @param options - The options for the cookie to set.
+ */
+export const setBrowserCookie = cookies.set;
diff --git a/packages/gitbook/src/lib/cookies.ts b/packages/gitbook/src/lib/cookies.ts
deleted file mode 100644
index 226b79381..000000000
--- a/packages/gitbook/src/lib/cookies.ts
+++ /dev/null
@@ -1,29 +0,0 @@
-import cookies from 'js-cookie';
-
-import { checkIsSecurityError } from './security-error';
-
-export function getAll(): {
- [key: string]: string;
-} {
- try {
- return cookies.get();
- } catch (error) {
- if (checkIsSecurityError(error)) {
- return {};
- }
- throw error;
- }
-}
-
-export function get(name: string): string | undefined {
- try {
- return cookies.get(name);
- } catch (error) {
- if (checkIsSecurityError(error)) {
- return undefined;
- }
- throw error;
- }
-}
-
-export const set = cookies.set;
diff --git a/packages/gitbook/src/lib/v1.ts b/packages/gitbook/src/lib/v1.ts
index 74ba53275..ed047c938 100644
--- a/packages/gitbook/src/lib/v1.ts
+++ b/packages/gitbook/src/lib/v1.ts
@@ -18,7 +18,6 @@ import {
getDocument,
getEmbedByUrlInSpace,
getLatestOpenAPISpecVersionContent,
- getPublishedContentByUrl,
getPublishedContentSite,
getReusableContent,
getRevision,
@@ -73,11 +72,7 @@ export async function getV1BaseContext(): Promise {
* This data fetcher should only be used at the top of the tree.
*/
async function getDataFetcherV1(): Promise {
- const apiClient = await api();
-
const dataFetcher: GitBookDataFetcher = {
- apiEndpoint: apiClient.client.endpoint,
-
async api() {
const result = await api();
return result.client;
@@ -100,17 +95,6 @@ async function getDataFetcherV1(): Promise {
});
},
- // @ts-ignore - types are compatible enough, and this will not be called in v1 this way
- getPublishedContentByUrl(params) {
- return wrapDataFetcherError(async () => {
- return getPublishedContentByUrl(
- params.url,
- params.visitorAuthToken ?? undefined,
- params.redirectOnError ? true : undefined
- );
- });
- },
-
getPublishedContentSite(params) {
return wrapDataFetcherError(async () => {
return getPublishedContentSite(params);
diff --git a/packages/gitbook/src/lib/visitor-token.test.ts b/packages/gitbook/src/lib/visitor-token.test.ts
index 1867f45f4..a1f07d250 100644
--- a/packages/gitbook/src/lib/visitor-token.test.ts
+++ b/packages/gitbook/src/lib/visitor-token.test.ts
@@ -1,5 +1,4 @@
import { describe, expect, it } from 'bun:test';
-import type { NextRequest } from 'next/server';
import type { JwtPayload } from 'jwt-decode';
import {
@@ -11,66 +10,94 @@ import {
describe('getVisitorAuthToken', () => {
it('should return the token from the query parameters', () => {
- const request = nextRequest('https://example.com?jwt_token=123');
- expect(getVisitorToken(request, request.nextUrl)).toEqual({ source: 'url', token: '123' });
+ expect(
+ getVisitorToken({ cookies: [], url: new URL('https://example.com?jwt_token=123') })
+ ).toEqual({ source: 'url', token: '123' });
});
it('should return the token from the cookie root basepath', () => {
- const request = nextRequest('https://example.com', {
- [getVisitorAuthCookieName('/')]: { value: getVisitorAuthCookieValue('/', '123') },
+ const visitorAuth = getVisitorToken({
+ cookies: [
+ {
+ name: getVisitorAuthCookieName('/'),
+ value: getVisitorAuthCookieValue('/', '123'),
+ },
+ ],
+ url: new URL('https://example.com'),
});
- const visitorAuth = getVisitorToken(request, request.nextUrl);
assertVisitorAuthCookieValue(visitorAuth);
expect(visitorAuth.token).toEqual('123');
});
it('should return the token from the cookie root basepath for a sub-path', () => {
- const request = nextRequest('https://example.com/hello/world', {
- [getVisitorAuthCookieName('/')]: { value: getVisitorAuthCookieValue('/', '123') },
+ const visitorAuth = getVisitorToken({
+ cookies: [
+ {
+ name: getVisitorAuthCookieName('/'),
+ value: getVisitorAuthCookieValue('/', '123'),
+ },
+ ],
+ url: new URL('https://example.com/hello/world'),
});
- const visitorAuth = getVisitorToken(request, request.nextUrl);
assertVisitorAuthCookieValue(visitorAuth);
expect(visitorAuth.token).toEqual('123');
});
-
it('should return the closest token from the path', () => {
- const request = nextRequest('https://example.com/hello/world', {
- [getVisitorAuthCookieName('/')]: { value: getVisitorAuthCookieValue('/', 'no') },
- [getVisitorAuthCookieName('/hello/')]: {
- value: getVisitorAuthCookieValue('/hello/', '123'),
- },
+ const visitorAuth = getVisitorToken({
+ cookies: [
+ {
+ name: getVisitorAuthCookieName('/'),
+ value: getVisitorAuthCookieValue('/', 'no'),
+ },
+ {
+ name: getVisitorAuthCookieName('/hello/'),
+ value: getVisitorAuthCookieValue('/hello/', '123'),
+ },
+ ],
+ url: new URL('https://example.com/hello/world'),
});
- const visitorAuth = getVisitorToken(request, request.nextUrl);
assertVisitorAuthCookieValue(visitorAuth);
expect(visitorAuth.token).toEqual('123');
});
it('should return the token from the cookie in a collection type url', () => {
- const request = nextRequest('https://example.com/hello/v/space1/cool', {
- [getVisitorAuthCookieName('/hello/v/space1/')]: {
- value: getVisitorAuthCookieValue('/hello/v/space1/', '123'),
- },
+ const visitorAuth = getVisitorToken({
+ cookies: [
+ {
+ name: getVisitorAuthCookieName('/hello/v/space1/'),
+ value: getVisitorAuthCookieValue('/hello/v/space1/', '123'),
+ },
+ ],
+ url: new URL('https://example.com/hello/v/space1/cool'),
});
- const visitorAuth = getVisitorToken(request, request.nextUrl);
assertVisitorAuthCookieValue(visitorAuth);
expect(visitorAuth.token).toEqual('123');
});
it('should return undefined if no cookie and no query param', () => {
- const request = nextRequest('https://example.com');
- expect(getVisitorToken(request, request.nextUrl)).toBeUndefined();
+ expect(
+ getVisitorToken({
+ cookies: [],
+ url: new URL('https://example.com'),
+ })
+ ).toBeUndefined();
});
// For backwards compatibility
it('should return the token from the cookie of a /v/ path when the url does not have /v/', () => {
- const request = nextRequest('https://example.com/hello/space1/cool', {
- [getVisitorAuthCookieName('/')]: { value: getVisitorAuthCookieValue('/', 'no') },
- [getVisitorAuthCookieName('/hello/v/space1/')]: {
- value: getVisitorAuthCookieValue('/hello/v/space1/', 'gotcha'),
- },
+ const visitorAuth = getVisitorToken({
+ cookies: [
+ {
+ name: getVisitorAuthCookieName('/'),
+ value: getVisitorAuthCookieValue('/', 'no'),
+ },
+ {
+ name: getVisitorAuthCookieName('/hello/v/space1/'),
+ value: getVisitorAuthCookieValue('/hello/v/space1/', 'gotcha'),
+ },
+ ],
+ url: new URL('https://example.com/hello/space1/cool'),
});
-
- const visitorAuth = getVisitorToken(request, request.nextUrl);
assertVisitorAuthCookieValue(visitorAuth);
expect(visitorAuth.token).toEqual('gotcha');
});
@@ -131,14 +158,3 @@ function assertVisitorAuthCookieValue(
throw new Error('Expected a VisitorAuthCookieValue');
}
-
-function nextRequest(url: string, cookies: Record = {}) {
- const nextUrl = new URL(url);
- // @ts-ignore
- return {
- url: nextUrl.toString(),
- nextUrl,
- headers: new Headers(),
- cookies: Object.entries(cookies),
- } as NextRequest;
-}
diff --git a/packages/gitbook/src/lib/visitor-token.ts b/packages/gitbook/src/lib/visitor-token.ts
index 6b652ceed..3350da828 100644
--- a/packages/gitbook/src/lib/visitor-token.ts
+++ b/packages/gitbook/src/lib/visitor-token.ts
@@ -5,7 +5,11 @@ import hash from 'object-hash';
const VISITOR_AUTH_PARAM = 'jwt_token';
export const VISITOR_TOKEN_COOKIE = 'gitbook-visitor-token';
+/**
+ * Typing for a cookie, matching the internal type of Next.js.
+ */
export type ResponseCookie = {
+ name: string;
value: string;
options?: Partial<{
httpOnly: boolean;
@@ -15,7 +19,8 @@ export type ResponseCookie = {
}>;
};
-export type ResponseCookies = Record;
+export type ResponseCookies = ResponseCookie[];
+export type RequestCookies = ResponseCookies;
/**
* The contents of the visitor authentication cookie.
@@ -52,10 +57,13 @@ export type VisitorTokenLookup =
* Get the visitor token for the request. This token can either be in the
* query parameters or stored as a cookie.
*/
-export function getVisitorToken(
- request: NextRequest,
- url: URL | NextRequest['nextUrl']
-): VisitorTokenLookup {
+export function getVisitorToken({
+ cookies,
+ url,
+}: {
+ cookies: RequestCookies;
+ url: URL | NextRequest['nextUrl'];
+}): VisitorTokenLookup {
const fromUrl = url.searchParams.get(VISITOR_AUTH_PARAM);
// Allow the empty string to come through
@@ -63,27 +71,26 @@ export function getVisitorToken(
return { source: 'url', token: fromUrl };
}
- const visitorAuthToken = getVisitorAuthTokenFromCookies(request, url);
+ const visitorAuthToken = getVisitorAuthTokenFromCookies(cookies, url);
if (visitorAuthToken) {
return { source: 'visitor-auth-cookie', ...visitorAuthToken };
}
- const visitorCustomToken = getVisitorCustomTokenFromCookies(request);
+ const visitorCustomToken = getVisitorCustomTokenFromCookies(cookies);
if (visitorCustomToken) {
return { source: 'gitbook-visitor-cookie', token: visitorCustomToken };
}
}
/**
- * Return the lookup result for content served with visitor auth. It basically disables caching
- * and sets a cookie with the visitor auth token.
+ * Return the lookup result for content served with visitor auth.
*/
export function getResponseCookiesForVisitorAuth(
basePath: string,
visitorTokenLookup: VisitorTokenLookup
): ResponseCookies {
if (!visitorTokenLookup) {
- return {};
+ return [];
}
let decoded: JwtPayload;
@@ -91,32 +98,35 @@ export function getResponseCookiesForVisitorAuth(
decoded = jwtDecode(visitorTokenLookup.token);
} catch (error) {
console.error('Error decoding visitor token', error);
- return {};
+ return [];
}
- return {
- /**
- * If the visitor token has been retrieve from the URL, or if its a VA cookie and the basePath is the same, set it
- * as a cookie on the response.
- *
- * Note that we do not re-store the gitbook-visitor-cookie in another cookie, to maintain a single source of truth.
- */
- ...(visitorTokenLookup?.source === 'url' ||
+ /**
+ * If the visitor token has been retrieve from the URL, or if its a VA cookie and the basePath is the same, set it
+ * as a cookie on the response.
+ *
+ * Note that we do not re-store the gitbook-visitor-cookie in another cookie, to maintain a single source of truth.
+ */
+ if (
+ visitorTokenLookup?.source === 'url' ||
(visitorTokenLookup?.source === 'visitor-auth-cookie' &&
visitorTokenLookup.basePath === basePath)
- ? {
- [getVisitorAuthCookieName(basePath)]: {
- value: getVisitorAuthCookieValue(basePath, visitorTokenLookup.token),
- options: {
- httpOnly: true,
- sameSite: process.env.NODE_ENV === 'production' ? 'none' : undefined,
- secure: process.env.NODE_ENV === 'production',
- maxAge: getVisitorAuthCookieMaxAge(decoded),
- },
- },
- }
- : {}),
- };
+ ) {
+ return [
+ {
+ name: getVisitorAuthCookieName(basePath),
+ value: getVisitorAuthCookieValue(basePath, visitorTokenLookup.token),
+ options: {
+ httpOnly: true,
+ sameSite: process.env.NODE_ENV === 'production' ? 'none' : undefined,
+ secure: process.env.NODE_ENV === 'production',
+ maxAge: getVisitorAuthCookieMaxAge(decoded),
+ },
+ },
+ ];
+ }
+
+ return [];
}
/**
@@ -180,7 +190,7 @@ function getUrlBasePathCombinations(url: URL | NextRequest['nextUrl']): string[]
* best possible match for the current URL.
*/
function getVisitorAuthTokenFromCookies(
- request: NextRequest,
+ cookies: RequestCookies,
url: URL | NextRequest['nextUrl']
): VisitorAuthCookieValue | undefined {
const urlBasePaths = getUrlBasePathCombinations(url);
@@ -188,7 +198,7 @@ function getVisitorAuthTokenFromCookies(
// for the content could be hosted on a base path like `/foo/v/bar` or `/foo` or just `/`
// We keep trying to find with each of these base paths until we find a token.
for (const basePath of urlBasePaths) {
- const found = findVisitorAuthCookieForBasePath(request, basePath);
+ const found = findVisitorAuthCookieForBasePath(cookies, basePath);
if (found) {
return found;
}
@@ -205,32 +215,27 @@ function getVisitorAuthTokenFromCookies(
*
* The cookie should contain as value a JWT encoded token that contains the claims of the visitor.
*/
-function getVisitorCustomTokenFromCookies(request: NextRequest): string | undefined {
- const visitorCustomCookie = Array.from(request.cookies).find(
- ([, cookie]) => cookie.name === VISITOR_TOKEN_COOKIE
- );
- return visitorCustomCookie ? visitorCustomCookie[1].value : undefined;
+function getVisitorCustomTokenFromCookies(cookies: RequestCookies): string | undefined {
+ const visitorCustomCookie = cookies.find((cookie) => cookie.name === VISITOR_TOKEN_COOKIE);
+ return visitorCustomCookie ? visitorCustomCookie.value : undefined;
}
/**
* Loop through all cookies and find the visitor authentication token for a given base path.
*/
function findVisitorAuthCookieForBasePath(
- request: NextRequest,
+ cookies: RequestCookies,
basePath: string
): VisitorAuthCookieValue | undefined {
- return Array.from(request.cookies).reduce(
- (acc, [name, cookie]) => {
- if (name === getVisitorAuthCookieName(basePath)) {
- const value = JSON.parse(cookie.value) as VisitorAuthCookieValue;
- if (value.basePath === basePath) {
- acc = value;
- }
+ return cookies.reduce((acc, cookie) => {
+ if (cookie.name === getVisitorAuthCookieName(basePath)) {
+ const value = JSON.parse(cookie.value) as VisitorAuthCookieValue;
+ if (value.basePath === basePath) {
+ acc = value;
}
- return acc;
- },
- undefined
- );
+ }
+ return acc;
+ }, undefined);
}
/**
diff --git a/packages/gitbook/src/middleware.ts b/packages/gitbook/src/middleware.ts
index 4a3ccf86d..20d978333 100644
--- a/packages/gitbook/src/middleware.ts
+++ b/packages/gitbook/src/middleware.ts
@@ -2,7 +2,6 @@ import { type ContentAPITokenPayload, CustomizationThemeMode, GitBookAPI } from
import { getURLLookupAlternatives, normalizeURL } from '@v2/lib/data';
import assertNever from 'assert-never';
import jwt from 'jsonwebtoken';
-import type { ResponseCookie } from 'next/dist/compiled/@edge-runtime/cookies';
import { type NextRequest, NextResponse } from 'next/server';
import hash from 'object-hash';
@@ -183,10 +182,6 @@ export async function middleware(request: NextRequest) {
headers.set(MiddlewareHeaders.SiteURLData, JSON.stringify(resolved));
}
- if (resolved.visitorToken) {
- headers.set(MiddlewareHeaders.VisitorAuthToken, resolved.visitorToken);
- }
-
// For tests, we make it possible to enable search indexation
// using a query parameter.
const xGitBookSearchIndexation =
@@ -395,7 +390,10 @@ async function lookupSiteInProxy(request: NextRequest, url: URL): Promise {
- const visitorAuthToken = getVisitorToken(request, url);
+ const visitorAuthToken = getVisitorToken({
+ cookies: request.cookies.getAll(),
+ url,
+ });
const lookup = await lookupSiteByAPI(url, visitorAuthToken);
return {
...lookup,
@@ -525,8 +523,9 @@ async function lookupSiteOrSpaceInMultiIdMode(
);
}
- const cookies: ResponseCookies = {
- [cookieName]: {
+ const cookies: ResponseCookies = [
+ {
+ name: cookieName,
value: encodeGitBookTokenCookie(source.id, apiToken, apiEndpoint),
options: {
httpOnly: true,
@@ -535,7 +534,7 @@ async function lookupSiteOrSpaceInMultiIdMode(
sameSite: process.env.NODE_ENV === 'production' ? 'none' : undefined,
},
},
- };
+ ];
// Get rid of the token from the URL
if (url.searchParams.has(AUTH_TOKEN_QUERY) || url.searchParams.has(API_ENDPOINT_QUERY)) {
@@ -607,7 +606,10 @@ async function lookupSiteInMultiPathMode(request: NextRequest, url: URL): Promis
const target = new URL(targetStr);
target.search = url.search;
- const visitorAuthToken = getVisitorToken(request, target);
+ const visitorAuthToken = getVisitorToken({
+ cookies: request.cookies.getAll(),
+ url: target,
+ });
const lookup = await lookupSiteByAPI(target, visitorAuthToken);
if ('error' in lookup) {
@@ -859,18 +861,9 @@ function encodeGitBookTokenCookie(
return JSON.stringify({ s: spaceId, t: token, e: apiEndpoint });
}
-function writeCookies(
- response: R,
- cookies: Record<
- string,
- {
- value: string;
- options?: Partial;
- }
- > = {}
-): R {
- Object.entries(cookies).forEach(([key, { value, options }]) => {
- response.cookies.set(key, value, options);
+function writeCookies(response: R, cookies: ResponseCookies = []): R {
+ cookies.forEach((cookie) => {
+ response.cookies.set(cookie.name, cookie.value, cookie.options);
});
return response;