diff --git a/bun.lock b/bun.lock index 2eb356b9a..e3f990b97 100644 --- a/bun.lock +++ b/bun.lock @@ -136,14 +136,17 @@ "@gitbook/api": "*", "@gitbook/cache-tags": "workspace:*", "@sindresorhus/fnv1a": "^3.1.0", + "jwt-decode": "^4.0.0", "next": "^15.2.1", "react": "^19.0.0", "react-dom": "^19.0.0", + "rison": "^0.1.1", "server-only": "^0.0.1", "warn-once": "^0.1.1", }, "devDependencies": { "@opennextjs/cloudflare": "^0.5.10", + "@types/rison": "^0.0.9", "gitbook": "*", "postcss": "^8", "tailwindcss": "^3.4.0", @@ -253,7 +256,7 @@ }, "overrides": { "@codemirror/state": "6.4.1", - "@gitbook/api": "0.101.0", + "@gitbook/api": "0.102.0", "react": "18.3.1", "react-dom": "18.3.1", }, @@ -604,7 +607,7 @@ "@fortawesome/fontawesome-svg-core": ["@fortawesome/fontawesome-svg-core@6.6.0", "", { "dependencies": { "@fortawesome/fontawesome-common-types": "6.6.0" } }, "sha512-KHwPkCk6oRT4HADE7smhfsKudt9N/9lm6EJ5BVg0tD1yPA5hht837fB87F8pn15D8JfTqQOjhKTktwmLMiD7Kg=="], - "@gitbook/api": ["@gitbook/api@0.101.0", "", { "dependencies": { "event-iterator": "^2.0.0", "eventsource-parser": "^3.0.0" } }, "sha512-DX8CyoXCRyqXW738G9Ik9Pq97L1RjFdVCGxska9tFkrn1+bf60mwpt+6669wrRdlBxb1t2Ru4DsGuUksxCKDag=="], + "@gitbook/api": ["@gitbook/api@0.102.0", "", { "dependencies": { "event-iterator": "^2.0.0", "eventsource-parser": "^3.0.0" } }, "sha512-9wPr5kyCHhTTwkaCYEMT4q3JjEl1q9mytgvpeusmeREfG4E0RZHYPEL8bmumO7T9wH/0vHjpfW2LSTUUs60AwQ=="], "@gitbook/cache-do": ["@gitbook/cache-do@workspace:packages/cache-do"], diff --git a/package.json b/package.json index b7150df9f..72ed2b1ba 100644 --- a/package.json +++ b/package.json @@ -12,7 +12,7 @@ "@codemirror/state": "6.4.1", "react": "18.3.1", "react-dom": "18.3.1", - "@gitbook/api": "0.101.0" + "@gitbook/api": "0.102.0" }, "private": true, "scripts": { diff --git a/packages/gitbook-v2/next.config.mjs b/packages/gitbook-v2/next.config.mjs index ee02a27f4..0361a6937 100644 --- a/packages/gitbook-v2/next.config.mjs +++ b/packages/gitbook-v2/next.config.mjs @@ -5,6 +5,7 @@ */ const nextConfig = { experimental: { + authInterrupts: true, useCache: true, }, diff --git a/packages/gitbook-v2/package.json b/packages/gitbook-v2/package.json index 8a060d2e1..3f85d65c4 100644 --- a/packages/gitbook-v2/package.json +++ b/packages/gitbook-v2/package.json @@ -10,11 +10,14 @@ "@gitbook/cache-tags": "workspace:*", "@sindresorhus/fnv1a": "^3.1.0", "server-only": "^0.0.1", - "warn-once": "^0.1.1" + "warn-once": "^0.1.1", + "rison": "^0.1.1", + "jwt-decode": "^4.0.0" }, "devDependencies": { "gitbook": "*", "@opennextjs/cloudflare": "^0.5.10", + "@types/rison": "^0.0.9", "tailwindcss": "^3.4.0", "postcss": "^8" }, diff --git a/packages/gitbook-v2/src/app/sites/dynamic/[mode]/[siteURL]/(content)/[pagePath]/loading.tsx b/packages/gitbook-v2/src/app/sites/dynamic/[mode]/[siteURL]/[siteData]/(content)/[pagePath]/loading.tsx similarity index 100% rename from packages/gitbook-v2/src/app/sites/dynamic/[mode]/[siteURL]/(content)/[pagePath]/loading.tsx rename to packages/gitbook-v2/src/app/sites/dynamic/[mode]/[siteURL]/[siteData]/(content)/[pagePath]/loading.tsx diff --git a/packages/gitbook-v2/src/app/sites/dynamic/[mode]/[siteURL]/(content)/[pagePath]/not-found.tsx b/packages/gitbook-v2/src/app/sites/dynamic/[mode]/[siteURL]/[siteData]/(content)/[pagePath]/not-found.tsx similarity index 100% rename from packages/gitbook-v2/src/app/sites/dynamic/[mode]/[siteURL]/(content)/[pagePath]/not-found.tsx rename to packages/gitbook-v2/src/app/sites/dynamic/[mode]/[siteURL]/[siteData]/(content)/[pagePath]/not-found.tsx diff --git a/packages/gitbook-v2/src/app/sites/dynamic/[mode]/[siteURL]/(content)/[pagePath]/page.tsx b/packages/gitbook-v2/src/app/sites/dynamic/[mode]/[siteURL]/[siteData]/(content)/[pagePath]/page.tsx similarity index 83% rename from packages/gitbook-v2/src/app/sites/dynamic/[mode]/[siteURL]/(content)/[pagePath]/page.tsx rename to packages/gitbook-v2/src/app/sites/dynamic/[mode]/[siteURL]/[siteData]/(content)/[pagePath]/page.tsx index 609057393..e5f520d39 100644 --- a/packages/gitbook-v2/src/app/sites/dynamic/[mode]/[siteURL]/(content)/[pagePath]/page.tsx +++ b/packages/gitbook-v2/src/app/sites/dynamic/[mode]/[siteURL]/[siteData]/(content)/[pagePath]/page.tsx @@ -13,20 +13,20 @@ type PageProps = { export default async function Page(props: PageProps) { const params = await props.params; - const context = await getDynamicSiteContext(params); + const { context } = await getDynamicSiteContext(params); const pathname = getPagePathFromParams(params); return ; } export async function generateViewport(props: PageProps): Promise { - const context = await getDynamicSiteContext(await props.params); + const { context } = await getDynamicSiteContext(await props.params); return generateSitePageViewport(context); } export async function generateMetadata(props: PageProps): Promise { const params = await props.params; - const context = await getDynamicSiteContext(params); + const { context } = await getDynamicSiteContext(params); const pathname = getPagePathFromParams(params); return generateSitePageMetadata({ diff --git a/packages/gitbook-v2/src/app/sites/dynamic/[mode]/[siteURL]/(content)/layout.tsx b/packages/gitbook-v2/src/app/sites/dynamic/[mode]/[siteURL]/[siteData]/(content)/layout.tsx similarity index 74% rename from packages/gitbook-v2/src/app/sites/dynamic/[mode]/[siteURL]/(content)/layout.tsx rename to packages/gitbook-v2/src/app/sites/dynamic/[mode]/[siteURL]/[siteData]/(content)/layout.tsx index ae7727abf..8a21a46db 100644 --- a/packages/gitbook-v2/src/app/sites/dynamic/[mode]/[siteURL]/(content)/layout.tsx +++ b/packages/gitbook-v2/src/app/sites/dynamic/[mode]/[siteURL]/[siteData]/(content)/layout.tsx @@ -6,7 +6,7 @@ import { } from '@/components/SiteLayout'; import { type RouteLayoutParams, getDynamicSiteContext } from '@v2/app/utils'; import { GITBOOK_DISABLE_TRACKING } from '@v2/lib/env'; -import { getThemeFromMiddleware, getVisitorAuthTokenFromMiddleware } from '@v2/lib/middleware'; +import { getThemeFromMiddleware } from '@v2/lib/middleware'; interface SiteDynamicLayoutProps { params: Promise; @@ -16,9 +16,8 @@ export default async function SiteDynamicLayout({ params, children, }: React.PropsWithChildren) { - const context = await getDynamicSiteContext(await params); + const { context, visitorAuthClaims } = await getDynamicSiteContext(await params); const forcedTheme = await getThemeFromMiddleware(); - const visitorAuthToken = await getVisitorAuthTokenFromMiddleware(); return ( @@ -26,7 +25,7 @@ export default async function SiteDynamicLayout({ context={context} forcedTheme={forcedTheme} withTracking={!GITBOOK_DISABLE_TRACKING} - visitorAuthToken={visitorAuthToken} + visitorAuthClaims={visitorAuthClaims} > {children} @@ -35,11 +34,11 @@ export default async function SiteDynamicLayout({ } export async function generateViewport({ params }: SiteDynamicLayoutProps) { - const context = await getDynamicSiteContext(await params); + const { context } = await getDynamicSiteContext(await params); return generateSiteLayoutViewport(context); } export async function generateMetadata({ params }: SiteDynamicLayoutProps) { - const context = await getDynamicSiteContext(await params); + const { context } = await getDynamicSiteContext(await params); return generateSiteLayoutMetadata(context); } diff --git a/packages/gitbook-v2/src/app/sites/dynamic/[mode]/[siteURL]/llms.txt/route.ts b/packages/gitbook-v2/src/app/sites/dynamic/[mode]/[siteURL]/[siteData]/llms.txt/route.ts similarity index 82% rename from packages/gitbook-v2/src/app/sites/dynamic/[mode]/[siteURL]/llms.txt/route.ts rename to packages/gitbook-v2/src/app/sites/dynamic/[mode]/[siteURL]/[siteData]/llms.txt/route.ts index 1da96a21c..634f29fb3 100644 --- a/packages/gitbook-v2/src/app/sites/dynamic/[mode]/[siteURL]/llms.txt/route.ts +++ b/packages/gitbook-v2/src/app/sites/dynamic/[mode]/[siteURL]/[siteData]/llms.txt/route.ts @@ -7,6 +7,6 @@ export async function GET( _request: NextRequest, { params }: { params: Promise } ) { - const context = await getDynamicSiteContext(await params); + const { context } = await getDynamicSiteContext(await params); return serveLLMsTxt(context); } diff --git a/packages/gitbook-v2/src/app/sites/dynamic/[mode]/[siteURL]/robots.txt/route.ts b/packages/gitbook-v2/src/app/sites/dynamic/[mode]/[siteURL]/[siteData]/robots.txt/route.ts similarity index 83% rename from packages/gitbook-v2/src/app/sites/dynamic/[mode]/[siteURL]/robots.txt/route.ts rename to packages/gitbook-v2/src/app/sites/dynamic/[mode]/[siteURL]/[siteData]/robots.txt/route.ts index d4c4da2b3..e9f8d83e6 100644 --- a/packages/gitbook-v2/src/app/sites/dynamic/[mode]/[siteURL]/robots.txt/route.ts +++ b/packages/gitbook-v2/src/app/sites/dynamic/[mode]/[siteURL]/[siteData]/robots.txt/route.ts @@ -7,6 +7,6 @@ export async function GET( _request: NextRequest, { params }: { params: Promise } ) { - const context = await getDynamicSiteContext(await params); + const { context } = await getDynamicSiteContext(await params); return serveRobotsTxt(context); } diff --git a/packages/gitbook-v2/src/app/sites/dynamic/[mode]/[siteURL]/sitemap-pages.xml/route.ts b/packages/gitbook-v2/src/app/sites/dynamic/[mode]/[siteURL]/[siteData]/sitemap-pages.xml/route.ts similarity index 83% rename from packages/gitbook-v2/src/app/sites/dynamic/[mode]/[siteURL]/sitemap-pages.xml/route.ts rename to packages/gitbook-v2/src/app/sites/dynamic/[mode]/[siteURL]/[siteData]/sitemap-pages.xml/route.ts index f56de486d..9ca612598 100644 --- a/packages/gitbook-v2/src/app/sites/dynamic/[mode]/[siteURL]/sitemap-pages.xml/route.ts +++ b/packages/gitbook-v2/src/app/sites/dynamic/[mode]/[siteURL]/[siteData]/sitemap-pages.xml/route.ts @@ -7,6 +7,6 @@ export async function GET( _request: NextRequest, { params }: { params: Promise } ) { - const context = await getDynamicSiteContext(await params); + const { context } = await getDynamicSiteContext(await params); return servePagesSitemap(context); } diff --git a/packages/gitbook-v2/src/app/sites/dynamic/[mode]/[siteURL]/sitemap.xml/route.ts b/packages/gitbook-v2/src/app/sites/dynamic/[mode]/[siteURL]/[siteData]/sitemap.xml/route.ts similarity index 83% rename from packages/gitbook-v2/src/app/sites/dynamic/[mode]/[siteURL]/sitemap.xml/route.ts rename to packages/gitbook-v2/src/app/sites/dynamic/[mode]/[siteURL]/[siteData]/sitemap.xml/route.ts index ee16aa169..458ab27db 100644 --- a/packages/gitbook-v2/src/app/sites/dynamic/[mode]/[siteURL]/sitemap.xml/route.ts +++ b/packages/gitbook-v2/src/app/sites/dynamic/[mode]/[siteURL]/[siteData]/sitemap.xml/route.ts @@ -7,6 +7,6 @@ export async function GET( _request: NextRequest, { params }: { params: Promise } ) { - const context = await getDynamicSiteContext(await params); + const { context } = await getDynamicSiteContext(await params); return serveRootSitemap(context); } diff --git a/packages/gitbook-v2/src/app/sites/dynamic/[mode]/[siteURL]/~gitbook/icon/route.ts b/packages/gitbook-v2/src/app/sites/dynamic/[mode]/[siteURL]/[siteData]/~gitbook/icon/route.ts similarity index 82% rename from packages/gitbook-v2/src/app/sites/dynamic/[mode]/[siteURL]/~gitbook/icon/route.ts rename to packages/gitbook-v2/src/app/sites/dynamic/[mode]/[siteURL]/[siteData]/~gitbook/icon/route.ts index 631a8d40b..cd6a118f7 100644 --- a/packages/gitbook-v2/src/app/sites/dynamic/[mode]/[siteURL]/~gitbook/icon/route.ts +++ b/packages/gitbook-v2/src/app/sites/dynamic/[mode]/[siteURL]/[siteData]/~gitbook/icon/route.ts @@ -7,6 +7,6 @@ export async function GET( request: NextRequest, { params }: { params: Promise } ) { - const context = await getDynamicSiteContext(await params); + const { context } = await getDynamicSiteContext(await params); return serveIcon(context, request); } diff --git a/packages/gitbook-v2/src/app/sites/dynamic/[mode]/[siteURL]/~gitbook/ogimage/[pageId]/route.ts b/packages/gitbook-v2/src/app/sites/dynamic/[mode]/[siteURL]/[siteData]/~gitbook/ogimage/[pageId]/route.ts similarity index 86% rename from packages/gitbook-v2/src/app/sites/dynamic/[mode]/[siteURL]/~gitbook/ogimage/[pageId]/route.ts rename to packages/gitbook-v2/src/app/sites/dynamic/[mode]/[siteURL]/[siteData]/~gitbook/ogimage/[pageId]/route.ts index 6d0d1fb39..6a25bad9a 100644 --- a/packages/gitbook-v2/src/app/sites/dynamic/[mode]/[siteURL]/~gitbook/ogimage/[pageId]/route.ts +++ b/packages/gitbook-v2/src/app/sites/dynamic/[mode]/[siteURL]/[siteData]/~gitbook/ogimage/[pageId]/route.ts @@ -8,6 +8,6 @@ export async function GET( _request: NextRequest, { params }: { params: Promise } ) { - const context = await getDynamicSiteContext(await params); + const { context } = await getDynamicSiteContext(await params); return serveOGImage(context, await params); } diff --git a/packages/gitbook-v2/src/app/sites/dynamic/[mode]/[siteURL]/~gitbook/pdf/layout.tsx b/packages/gitbook-v2/src/app/sites/dynamic/[mode]/[siteURL]/[siteData]/~gitbook/pdf/layout.tsx similarity index 84% rename from packages/gitbook-v2/src/app/sites/dynamic/[mode]/[siteURL]/~gitbook/pdf/layout.tsx rename to packages/gitbook-v2/src/app/sites/dynamic/[mode]/[siteURL]/[siteData]/~gitbook/pdf/layout.tsx index a80ea1913..068d740c0 100644 --- a/packages/gitbook-v2/src/app/sites/dynamic/[mode]/[siteURL]/~gitbook/pdf/layout.tsx +++ b/packages/gitbook-v2/src/app/sites/dynamic/[mode]/[siteURL]/[siteData]/~gitbook/pdf/layout.tsx @@ -6,7 +6,7 @@ export default async function RootLayout(props: { children: React.ReactNode; }) { const { params, children } = props; - const context = await getDynamicSiteContext(await params); + const { context } = await getDynamicSiteContext(await params); return {children}; } diff --git a/packages/gitbook-v2/src/app/sites/dynamic/[mode]/[siteURL]/~gitbook/pdf/page.tsx b/packages/gitbook-v2/src/app/sites/dynamic/[mode]/[siteURL]/[siteData]/~gitbook/pdf/page.tsx similarity index 80% rename from packages/gitbook-v2/src/app/sites/dynamic/[mode]/[siteURL]/~gitbook/pdf/page.tsx rename to packages/gitbook-v2/src/app/sites/dynamic/[mode]/[siteURL]/[siteData]/~gitbook/pdf/page.tsx index 964b79b66..3fa3b3ff0 100644 --- a/packages/gitbook-v2/src/app/sites/dynamic/[mode]/[siteURL]/~gitbook/pdf/page.tsx +++ b/packages/gitbook-v2/src/app/sites/dynamic/[mode]/[siteURL]/[siteData]/~gitbook/pdf/page.tsx @@ -6,7 +6,7 @@ export async function generateMetadata({ }: { params: Promise; }) { - const context = await getDynamicSiteContext(await params); + const { context } = await getDynamicSiteContext(await params); return generatePDFMetadata(context); } @@ -15,6 +15,6 @@ export default async function Page(props: { searchParams: Promise<{ [key: string]: string }>; }) { const { params, searchParams } = props; - const context = await getDynamicSiteContext(await params); + const { context } = await getDynamicSiteContext(await params); return ; } diff --git a/packages/gitbook-v2/src/app/sites/static/[mode]/[siteURL]/[pagePath]/not-found.tsx b/packages/gitbook-v2/src/app/sites/static/[mode]/[siteURL]/[siteData]/[pagePath]/not-found.tsx similarity index 100% rename from packages/gitbook-v2/src/app/sites/static/[mode]/[siteURL]/[pagePath]/not-found.tsx rename to packages/gitbook-v2/src/app/sites/static/[mode]/[siteURL]/[siteData]/[pagePath]/not-found.tsx diff --git a/packages/gitbook-v2/src/app/sites/static/[mode]/[siteURL]/[pagePath]/page.tsx b/packages/gitbook-v2/src/app/sites/static/[mode]/[siteURL]/[siteData]/[pagePath]/page.tsx similarity index 86% rename from packages/gitbook-v2/src/app/sites/static/[mode]/[siteURL]/[pagePath]/page.tsx rename to packages/gitbook-v2/src/app/sites/static/[mode]/[siteURL]/[siteData]/[pagePath]/page.tsx index 4939dcbb7..e5d4ebc21 100644 --- a/packages/gitbook-v2/src/app/sites/static/[mode]/[siteURL]/[pagePath]/page.tsx +++ b/packages/gitbook-v2/src/app/sites/static/[mode]/[siteURL]/[siteData]/[pagePath]/page.tsx @@ -18,7 +18,7 @@ export default async function Page(props: PageProps) { 'use cache'; const params = await props.params; - const context = await getStaticSiteContext(params); + const { context } = await getStaticSiteContext(params); const pathname = getPagePathFromParams(params); cacheTag( @@ -32,13 +32,13 @@ export default async function Page(props: PageProps) { } export async function generateViewport(props: PageProps): Promise { - const context = await getStaticSiteContext(await props.params); + const { context } = await getStaticSiteContext(await props.params); return generateSitePageViewport(context); } export async function generateMetadata(props: PageProps): Promise { const params = await props.params; - const context = await getStaticSiteContext(params); + const { context } = await getStaticSiteContext(params); const pathname = getPagePathFromParams(params); return generateSitePageMetadata({ diff --git a/packages/gitbook-v2/src/app/sites/static/[mode]/[siteURL]/layout.tsx b/packages/gitbook-v2/src/app/sites/static/[mode]/[siteURL]/[siteData]/layout.tsx similarity index 83% rename from packages/gitbook-v2/src/app/sites/static/[mode]/[siteURL]/layout.tsx rename to packages/gitbook-v2/src/app/sites/static/[mode]/[siteURL]/[siteData]/layout.tsx index 2d5de4284..86a6b867f 100644 --- a/packages/gitbook-v2/src/app/sites/static/[mode]/[siteURL]/layout.tsx +++ b/packages/gitbook-v2/src/app/sites/static/[mode]/[siteURL]/[siteData]/layout.tsx @@ -19,7 +19,7 @@ export default async function SiteStaticLayout({ }: React.PropsWithChildren) { 'use cache'; - const context = await getStaticSiteContext(await params); + const { context, visitorAuthClaims } = await getStaticSiteContext(await params); cacheTag( getCacheTag({ @@ -33,7 +33,7 @@ export default async function SiteStaticLayout({ {children} @@ -42,11 +42,11 @@ export default async function SiteStaticLayout({ } export async function generateViewport({ params }: SiteStaticLayoutProps) { - const context = await getStaticSiteContext(await params); + const { context } = await getStaticSiteContext(await params); return generateSiteLayoutViewport(context); } export async function generateMetadata({ params }: SiteStaticLayoutProps) { - const context = await getStaticSiteContext(await params); + const { context } = await getStaticSiteContext(await params); return generateSiteLayoutMetadata(context); } diff --git a/packages/gitbook-v2/src/app/sites/static/[mode]/[siteURL]/llms.txt/route.ts b/packages/gitbook-v2/src/app/sites/static/[mode]/[siteURL]/[siteData]/llms.txt/route.ts similarity index 84% rename from packages/gitbook-v2/src/app/sites/static/[mode]/[siteURL]/llms.txt/route.ts rename to packages/gitbook-v2/src/app/sites/static/[mode]/[siteURL]/[siteData]/llms.txt/route.ts index a53054904..cb4930c43 100644 --- a/packages/gitbook-v2/src/app/sites/static/[mode]/[siteURL]/llms.txt/route.ts +++ b/packages/gitbook-v2/src/app/sites/static/[mode]/[siteURL]/[siteData]/llms.txt/route.ts @@ -9,6 +9,6 @@ export async function GET( _request: NextRequest, { params }: { params: Promise } ) { - const context = await getStaticSiteContext(await params); + const { context } = await getStaticSiteContext(await params); return serveLLMsTxt(context); } diff --git a/packages/gitbook-v2/src/app/sites/static/[mode]/[siteURL]/robots.txt/route.ts b/packages/gitbook-v2/src/app/sites/static/[mode]/[siteURL]/[siteData]/robots.txt/route.ts similarity index 84% rename from packages/gitbook-v2/src/app/sites/static/[mode]/[siteURL]/robots.txt/route.ts rename to packages/gitbook-v2/src/app/sites/static/[mode]/[siteURL]/[siteData]/robots.txt/route.ts index 5e48abbba..95820d768 100644 --- a/packages/gitbook-v2/src/app/sites/static/[mode]/[siteURL]/robots.txt/route.ts +++ b/packages/gitbook-v2/src/app/sites/static/[mode]/[siteURL]/[siteData]/robots.txt/route.ts @@ -9,6 +9,6 @@ export async function GET( _request: NextRequest, { params }: { params: Promise } ) { - const context = await getStaticSiteContext(await params); + const { context } = await getStaticSiteContext(await params); return serveRobotsTxt(context); } diff --git a/packages/gitbook-v2/src/app/sites/static/[mode]/[siteURL]/sitemap-pages.xml/route.ts b/packages/gitbook-v2/src/app/sites/static/[mode]/[siteURL]/[siteData]/sitemap-pages.xml/route.ts similarity index 85% rename from packages/gitbook-v2/src/app/sites/static/[mode]/[siteURL]/sitemap-pages.xml/route.ts rename to packages/gitbook-v2/src/app/sites/static/[mode]/[siteURL]/[siteData]/sitemap-pages.xml/route.ts index 6993023d4..472ae6309 100644 --- a/packages/gitbook-v2/src/app/sites/static/[mode]/[siteURL]/sitemap-pages.xml/route.ts +++ b/packages/gitbook-v2/src/app/sites/static/[mode]/[siteURL]/[siteData]/sitemap-pages.xml/route.ts @@ -9,6 +9,6 @@ export async function GET( _request: NextRequest, { params }: { params: Promise } ) { - const context = await getStaticSiteContext(await params); + const { context } = await getStaticSiteContext(await params); return servePagesSitemap(context); } diff --git a/packages/gitbook-v2/src/app/sites/static/[mode]/[siteURL]/sitemap.xml/route.ts b/packages/gitbook-v2/src/app/sites/static/[mode]/[siteURL]/[siteData]/sitemap.xml/route.ts similarity index 85% rename from packages/gitbook-v2/src/app/sites/static/[mode]/[siteURL]/sitemap.xml/route.ts rename to packages/gitbook-v2/src/app/sites/static/[mode]/[siteURL]/[siteData]/sitemap.xml/route.ts index 2f820d394..eac5149cd 100644 --- a/packages/gitbook-v2/src/app/sites/static/[mode]/[siteURL]/sitemap.xml/route.ts +++ b/packages/gitbook-v2/src/app/sites/static/[mode]/[siteURL]/[siteData]/sitemap.xml/route.ts @@ -9,6 +9,6 @@ export async function GET( _request: NextRequest, { params }: { params: Promise } ) { - const context = await getStaticSiteContext(await params); + const { context } = await getStaticSiteContext(await params); return serveRootSitemap(context); } diff --git a/packages/gitbook-v2/src/app/sites/static/[mode]/[siteURL]/~gitbook/icon/route.ts b/packages/gitbook-v2/src/app/sites/static/[mode]/[siteURL]/[siteData]/~gitbook/icon/route.ts similarity index 84% rename from packages/gitbook-v2/src/app/sites/static/[mode]/[siteURL]/~gitbook/icon/route.ts rename to packages/gitbook-v2/src/app/sites/static/[mode]/[siteURL]/[siteData]/~gitbook/icon/route.ts index cdbbf9fb7..050aef9ef 100644 --- a/packages/gitbook-v2/src/app/sites/static/[mode]/[siteURL]/~gitbook/icon/route.ts +++ b/packages/gitbook-v2/src/app/sites/static/[mode]/[siteURL]/[siteData]/~gitbook/icon/route.ts @@ -9,6 +9,6 @@ export async function GET( request: NextRequest, { params }: { params: Promise } ) { - const context = await getStaticSiteContext(await params); + const { context } = await getStaticSiteContext(await params); return serveIcon(context, request); } diff --git a/packages/gitbook-v2/src/app/sites/static/[mode]/[siteURL]/~gitbook/ogimage/[pageId]/route.ts b/packages/gitbook-v2/src/app/sites/static/[mode]/[siteURL]/[siteData]/~gitbook/ogimage/[pageId]/route.ts similarity index 87% rename from packages/gitbook-v2/src/app/sites/static/[mode]/[siteURL]/~gitbook/ogimage/[pageId]/route.ts rename to packages/gitbook-v2/src/app/sites/static/[mode]/[siteURL]/[siteData]/~gitbook/ogimage/[pageId]/route.ts index 83bd0c07b..211de1727 100644 --- a/packages/gitbook-v2/src/app/sites/static/[mode]/[siteURL]/~gitbook/ogimage/[pageId]/route.ts +++ b/packages/gitbook-v2/src/app/sites/static/[mode]/[siteURL]/[siteData]/~gitbook/ogimage/[pageId]/route.ts @@ -10,6 +10,6 @@ export async function GET( _request: NextRequest, { params }: { params: Promise } ) { - const context = await getStaticSiteContext(await params); + const { context } = await getStaticSiteContext(await params); return serveOGImage(context, await params); } diff --git a/packages/gitbook-v2/src/app/utils.ts b/packages/gitbook-v2/src/app/utils.ts index 805d1c0f6..b6c4af2b4 100644 --- a/packages/gitbook-v2/src/app/utils.ts +++ b/packages/gitbook-v2/src/app/utils.ts @@ -1,9 +1,9 @@ -import { - fetchSiteContextByURL, - fetchSiteContextByURLLookup, - getBaseContext, -} from '@v2/lib/context'; -import { getSiteURLDataFromMiddleware } from '@v2/lib/middleware'; +import { getVisitorAuthClaims, getVisitorAuthClaimsFromToken } from '@/lib/adaptive'; +import type { PublishedSiteContent, SiteAPIToken } from '@gitbook/api'; +import { fetchSiteContextByURLLookup, getBaseContext } from '@v2/lib/context'; +import { jwtDecode } from 'jwt-decode'; +import { forbidden } from 'next/navigation'; +import rison from 'rison'; export type RouteParamMode = 'url-host' | 'url'; @@ -12,6 +12,9 @@ export type RouteLayoutParams = { /** URL encoded site URL */ siteURL: string; + + /** URL and Rison encoded site data from getPublishedContentByUrl */ + siteData: string; }; export type RouteParams = RouteLayoutParams & { @@ -21,19 +24,29 @@ export type RouteParams = RouteLayoutParams & { /** * Get the static context when rendering statically a site. */ -export function getStaticSiteContext(params: RouteLayoutParams) { +export async function getStaticSiteContext(params: RouteLayoutParams) { const siteURL = getSiteURLFromParams(params); - return fetchSiteContextByURL( + const siteURLData = getSiteURLDataFromParams(params); + + // For static routes, we check the expiration of the JWT token + // as the route might be revalidated after expiration + const decoded = jwtDecode(siteURLData.apiToken); + if (decoded.exp && decoded.exp < Date.now() / 1000 + 120) { + forbidden(); + } + + const context = await fetchSiteContextByURLLookup( getBaseContext({ siteURL, urlMode: getModeFromParams(params.mode), }), - { - url: siteURL.toString(), - visitorAuthToken: null, - redirectOnError: false, - } + siteURLData ); + + return { + context, + visitorAuthClaims: getVisitorAuthClaimsFromToken(decoded), + }; } /** @@ -42,15 +55,20 @@ export function getStaticSiteContext(params: RouteLayoutParams) { */ export async function getDynamicSiteContext(params: RouteLayoutParams) { const siteURL = getSiteURLFromParams(params); - const siteURLData = await getSiteURLDataFromMiddleware(); + const siteURLData = getSiteURLDataFromParams(params); - return fetchSiteContextByURLLookup( + const context = await fetchSiteContextByURLLookup( getBaseContext({ siteURL, urlMode: getModeFromParams(params.mode), }), siteURLData ); + + return { + context, + visitorAuthClaims: getVisitorAuthClaims(siteURLData), + }; } /** @@ -74,3 +92,11 @@ function getModeFromParams(mode: string): RouteParamMode { return 'url'; } + +/** + * Get the decoded site data from the params. + */ +function getSiteURLDataFromParams(params: RouteLayoutParams): PublishedSiteContent { + const decoded = decodeURIComponent(params.siteData); + return rison.decode(decoded); +} diff --git a/packages/gitbook-v2/src/lib/context.ts b/packages/gitbook-v2/src/lib/context.ts index e3f96b352..72eafb2a5 100644 --- a/packages/gitbook-v2/src/lib/context.ts +++ b/packages/gitbook-v2/src/lib/context.ts @@ -168,29 +168,6 @@ export function getLinkerForSiteURL(input: { return linker; } -/** - * Fetch the context of a site for a given URL and a base context. - */ -export async function fetchSiteContextByURL( - baseContext: GitBookBaseContext, - input: { - url: string; - visitorAuthToken: string | null; - redirectOnError: boolean; - } -): Promise { - const { dataFetcher } = baseContext; - const data = await throwIfDataError( - dataFetcher.getPublishedContentByUrl({ - url: input.url, - visitorAuthToken: input.visitorAuthToken, - redirectOnError: input.redirectOnError, - }) - ); - - return fetchSiteContextByURLLookup(baseContext, data); -} - /** * Fetch the context of a site using the resolution of a URL */ diff --git a/packages/gitbook-v2/src/lib/data/api.ts b/packages/gitbook-v2/src/lib/data/api.ts index 70b3a6266..fde9dba09 100644 --- a/packages/gitbook-v2/src/lib/data/api.ts +++ b/packages/gitbook-v2/src/lib/data/api.ts @@ -4,11 +4,7 @@ import { GitBookAPI, type GitBookAPIServiceBinding, } from '@gitbook/api'; -import { - getCacheTag, - getCacheTagForURL, - getComputedContentSourceCacheTags, -} from '@gitbook/cache-tags'; +import { getCacheTag, getComputedContentSourceCacheTags } from '@gitbook/cache-tags'; import { GITBOOK_API_TOKEN, GITBOOK_API_URL, GITBOOK_USER_AGENT } from '@v2/lib/env'; import { unstable_cacheLife as cacheLife, unstable_cacheTag as cacheTag } from 'next/cache'; import { wrapDataFetcherError } from './errors'; @@ -167,18 +163,6 @@ export function createDataFetcher( getUserById(userId) { return trace('getUserById', () => getUserById({ apiToken: null }, { userId })); }, - getPublishedContentByUrl(params) { - return trace('getPublishedContentByUrl', () => - getPublishedContentByUrl( - { apiToken: null }, - { - url: params.url, - visitorAuthToken: params.visitorAuthToken, - redirectOnError: params.redirectOnError, - } - ) - ); - }, }; } @@ -439,42 +423,6 @@ async function getLatestOpenAPISpecVersionContent( }); } -async function getPublishedContentByUrl( - input: DataFetcherInput, - params: { - url: string; - visitorAuthToken: string | null; - redirectOnError: boolean; - } -) { - 'use cache'; - - const { url, visitorAuthToken, redirectOnError } = params; - - cacheTag(getCacheTagForURL(url)); - cacheLife('days'); - - return wrapDataFetcherError(async () => { - const api = await apiClient(input); - const res = await api.urls.getPublishedContentByUrl({ - url, - visitorAuthToken: visitorAuthToken ?? undefined, - redirectOnError, - }); - - if ('site' in res.data) { - cacheTag( - getCacheTag({ - tag: 'site', - site: res.data.site, - }) - ); - } - - return res.data; - }); -} - async function getPublishedContentSite( input: DataFetcherInput, params: { diff --git a/packages/gitbook-v2/src/lib/data/lookup.ts b/packages/gitbook-v2/src/lib/data/lookup.ts index 5ae26a267..b6857cba2 100644 --- a/packages/gitbook-v2/src/lib/data/lookup.ts +++ b/packages/gitbook-v2/src/lib/data/lookup.ts @@ -94,12 +94,15 @@ export async function getPublishedContentByURL(input: { * In both cases, the idea is to use the deepest/longest/most inclusive path to resolve the content. */ if (alternative.primary || ('site' in data && data.complete)) { + const changeRequest = data.changeRequest ?? lookup.changeRequest; + const revision = data.revision ?? lookup.revision; + const siteResult: PublishedSiteContentLookup = { ...data, - changeRequest: data.changeRequest ?? lookup.changeRequest, - revision: data.revision ?? lookup.revision, basePath: joinPath(data.basePath, lookup.basePath ?? ''), pathname: joinPath(data.pathname, alternative.extraPath), + ...(changeRequest ? { changeRequest } : {}), + ...(revision ? { revision } : {}), }; return { data: siteResult }; } diff --git a/packages/gitbook-v2/src/lib/data/types.ts b/packages/gitbook-v2/src/lib/data/types.ts index 50cc83cbd..8272cef5a 100644 --- a/packages/gitbook-v2/src/lib/data/types.ts +++ b/packages/gitbook-v2/src/lib/data/types.ts @@ -37,15 +37,6 @@ export interface GitBookDataFetcher { */ getUserById(userId: string): Promise>; - /** - * Get a published content by its URL. - */ - getPublishedContentByUrl(params: { - url: string; - visitorAuthToken: string | null; - redirectOnError: boolean; - }): Promise>; - /** * Get a published content site by its organization ID and site ID. */ diff --git a/packages/gitbook-v2/src/lib/middleware.ts b/packages/gitbook-v2/src/lib/middleware.ts index 7111f4e73..8490b29c3 100644 --- a/packages/gitbook-v2/src/lib/middleware.ts +++ b/packages/gitbook-v2/src/lib/middleware.ts @@ -32,11 +32,6 @@ export enum MiddlewareHeaders { */ Customization = 'x-gitbook-customization', - /** - * The visitor token used to access this content - */ - VisitorAuthToken = 'x-gitbook-visitor-token', - /** * The API token used to fetch the content. * This should only be passed for non-site dynamic routes. @@ -105,20 +100,6 @@ export async function getThemeFromMiddleware() { : CustomizationThemeMode.Dark; } -/** - * Get the visitor auth token from the middleware headers. - * This function should only be called in a dynamic route. - */ -export async function getVisitorAuthTokenFromMiddleware(): Promise { - const headersList = await headers(); - const visitorAuthToken = headersList.get(MiddlewareHeaders.VisitorAuthToken); - if (!visitorAuthToken) { - return null; - } - - return visitorAuthToken; -} - /** * Get the API token from the middleware headers. * This function should only be called in a dynamic route. diff --git a/packages/gitbook-v2/src/middleware.ts b/packages/gitbook-v2/src/middleware.ts index 44fc2c312..a4c368754 100644 --- a/packages/gitbook-v2/src/middleware.ts +++ b/packages/gitbook-v2/src/middleware.ts @@ -1,6 +1,7 @@ import { CustomizationThemeMode } from '@gitbook/api'; import type { NextRequest } from 'next/server'; import { NextResponse } from 'next/server'; +import rison from 'rison'; import { getContentSecurityPolicy } from '@/lib/csp'; import { validateSerializedCustomization } from '@/lib/customization'; @@ -83,7 +84,10 @@ async function serveSiteRoutes(requestURL: URL, request: NextRequest) { // Detect and extract the visitor authentication token from the request // // @ts-ignore - request typing - const visitorToken = getVisitorToken(request, siteURL); + const visitorToken = getVisitorToken({ + cookies: request.cookies.getAll(), + url: siteURL, + }); const data = await throwIfDataError( getPublishedContentByURL({ @@ -95,7 +99,7 @@ async function serveSiteRoutes(requestURL: URL, request: NextRequest) { redirectOnError: visitorToken?.source === 'visitor-auth-cookie', }) ); - let cookies: ResponseCookies = {}; + const cookies: ResponseCookies = []; // // Handle redirects @@ -123,10 +127,7 @@ async function serveSiteRoutes(requestURL: URL, request: NextRequest) { return NextResponse.redirect(data.redirect); } - cookies = { - ...cookies, - ...getResponseCookiesForVisitorAuth(data.basePath, visitorToken), - }; + cookies.push(...getResponseCookiesForVisitorAuth(data.siteBasePath, visitorToken)); // // Make sure the URL is clean of any va token after a successful lookup @@ -144,8 +145,9 @@ async function serveSiteRoutes(requestURL: URL, request: NextRequest) { // Render and serve the content // - // When visitor has authentication (adaptive content or VA), we serve dynamic routes. - let routeType = visitorToken ? 'dynamic' : 'static'; + // The route is static, except when using dynamic parameters from query params + // (customization override, theme, etc) + let routeType: 'dynamic' | 'static' = 'static'; const requestHeaders = new Headers(request.headers); requestHeaders.set(MiddlewareHeaders.RouteType, routeType); @@ -184,6 +186,7 @@ async function serveSiteRoutes(requestURL: URL, request: NextRequest) { routeType, mode, encodeURIComponent(siteURLWithoutProtocol), + encodeURIComponent(rison.encode(data)), pathname, ].join('/'); @@ -234,8 +237,9 @@ async function servePreviewRoutes(requestURL: URL, request: NextRequest) { const queryAPIToken = requestURL.searchParams.get('token'); if (queryAPIToken) { requestURL.searchParams.delete('token'); - return writeResponseCookies(NextResponse.redirect(requestURL.toString()), { - [cookieName]: { + return writeResponseCookies(NextResponse.redirect(requestURL.toString()), [ + { + name: cookieName, value: queryAPIToken, options: { httpOnly: true, @@ -244,7 +248,7 @@ async function servePreviewRoutes(requestURL: URL, request: NextRequest) { maxAge: 60 * 60, // 1 hour }, }, - }); + ]); } const apiToken = request.cookies.get(cookieName)?.value; @@ -371,8 +375,8 @@ function appendQueryParams(url: URL, from: URLSearchParams) { * Write the cookies to a response. */ function writeResponseCookies(response: R, cookies: ResponseCookies): R { - Object.entries(cookies).forEach(([key, { value, options }]) => { - response.cookies.set(key, value, options); + cookies.forEach((cookie) => { + response.cookies.set(cookie.name, cookie.value, cookie.options); }); return response; diff --git a/packages/gitbook/src/app/middleware/(site)/(content)/layout.tsx b/packages/gitbook/src/app/middleware/(site)/(content)/layout.tsx index 3ac8b8427..10db3a345 100644 --- a/packages/gitbook/src/app/middleware/(site)/(content)/layout.tsx +++ b/packages/gitbook/src/app/middleware/(site)/(content)/layout.tsx @@ -1,4 +1,4 @@ -import { getThemeFromMiddleware, getVisitorAuthTokenFromMiddleware } from '@v2/lib/middleware'; +import { getSiteURLDataFromMiddleware, getThemeFromMiddleware } from '@v2/lib/middleware'; import type { Metadata, Viewport } from 'next'; import type React from 'react'; @@ -7,6 +7,7 @@ import { generateSiteLayoutMetadata, generateSiteLayoutViewport, } from '@/components/SiteLayout'; +import { getVisitorAuthClaims } from '@/lib/adaptive'; import { getSiteContentPointer } from '@/lib/pointer'; import { shouldTrackEvents } from '@/lib/tracking'; import { fetchV1ContextForSitePointer } from '@/lib/v1'; @@ -22,14 +23,14 @@ export default async function ContentLayout(props: { children: React.ReactNode } const context = await fetchLayoutData(); const queryStringTheme = await getThemeFromMiddleware(); - const visitorAuthToken = await getVisitorAuthTokenFromMiddleware(); + const siteData = await getSiteURLDataFromMiddleware(); return ( {children} diff --git a/packages/gitbook/src/components/Insights/InsightsProvider.tsx b/packages/gitbook/src/components/Insights/InsightsProvider.tsx index 8f3cae91b..002d7e00c 100644 --- a/packages/gitbook/src/components/Insights/InsightsProvider.tsx +++ b/packages/gitbook/src/components/Insights/InsightsProvider.tsx @@ -5,8 +5,8 @@ import { OpenAPIOperationContextProvider } from '@gitbook/react-openapi'; import * as React from 'react'; import { useDebounceCallback, useEventCallback } from 'usehooks-ts'; -import * as cookies from '@/lib/cookies'; - +import type { VisitorAuthClaims } from '@/lib/adaptive'; +import { getAllBrowserCookiesMap } from '@/lib/browser-cookies'; import { getSession } from './sessions'; import { getVisitorId } from './visitorId'; @@ -23,6 +23,7 @@ type InsightsEventContext = { siteShareKey: string | null; spaceId: string; revisionId: string; + visitorAuthClaims: VisitorAuthClaims; }; /** @@ -65,7 +66,6 @@ interface InsightsProviderProps extends InsightsEventContext { enabled: boolean; appURL: string; apiHost: string; - visitorAuthToken: string | null; children: React.ReactNode; } @@ -73,7 +73,7 @@ interface InsightsProviderProps extends InsightsEventContext { * Wrap the content of the app with the InsightsProvider to track events. */ export function InsightsProvider(props: InsightsProviderProps) { - const { enabled, appURL, apiHost, visitorAuthToken, children, ...context } = props; + const { enabled, appURL, apiHost, children, ...context } = props; const visitorIdRef = React.useRef(null); const eventsRef = React.useRef<{ @@ -116,7 +116,6 @@ export function InsightsProvider(props: InsightsProviderProps) { pageContext: eventsForPathname.pageContext, visitorId, sessionId: session.id, - visitorAuthToken, }) ); @@ -253,16 +252,15 @@ function transformEvents(input: { pageContext: InsightsEventPageContext; visitorId: string; sessionId: string; - visitorAuthToken: string | null; }): api.SiteInsightsEvent[] { const session: api.SiteInsightsEventSession = { sessionId: input.sessionId, visitorId: input.visitorId, userAgent: window.navigator.userAgent, language: window.navigator.language, - cookies: cookies.getAll(), + cookies: getAllBrowserCookiesMap(), referrer: document.referrer || null, - visitorAuthToken: input.visitorAuthToken ?? null, + visitorAuthClaims: input.context.visitorAuthClaims, }; const location: api.SiteInsightsEventLocation = { diff --git a/packages/gitbook/src/components/Insights/cookies.ts b/packages/gitbook/src/components/Insights/cookies.ts index f2208cb3a..bd491ddc4 100644 --- a/packages/gitbook/src/components/Insights/cookies.ts +++ b/packages/gitbook/src/components/Insights/cookies.ts @@ -1,6 +1,6 @@ 'use client'; -import * as cookies from '@/lib/cookies'; +import { getBrowserCookie, setBrowserCookie } from '@/lib/browser-cookies'; const GRANTED_COOKIE = '__gitbook_cookie_granted'; @@ -8,7 +8,7 @@ const GRANTED_COOKIE = '__gitbook_cookie_granted'; * Accept or reject cookies. */ export function setCookiesTracking(enabled: boolean) { - cookies.set(GRANTED_COOKIE, enabled ? 'yes' : 'no', { + setBrowserCookie(GRANTED_COOKIE, enabled ? 'yes' : 'no', { expires: 365, sameSite: 'none', secure: true, @@ -20,7 +20,7 @@ export function setCookiesTracking(enabled: boolean) { * Return `undefined` if state is not known. */ export function isCookiesTrackingDisabled() { - const state = cookies.get(GRANTED_COOKIE); + const state = getBrowserCookie(GRANTED_COOKIE); if (state === 'yes') { return false; diff --git a/packages/gitbook/src/components/Insights/visitorId.ts b/packages/gitbook/src/components/Insights/visitorId.ts index 704dadcf8..8c2b7d8dd 100644 --- a/packages/gitbook/src/components/Insights/visitorId.ts +++ b/packages/gitbook/src/components/Insights/visitorId.ts @@ -1,6 +1,6 @@ 'use client'; -import * as cookies from '@/lib/cookies'; +import { getBrowserCookie } from '@/lib/browser-cookies'; import { isCookiesTrackingDisabled } from './cookies'; import { generateRandomId } from './utils'; @@ -37,7 +37,7 @@ async function fetchVisitorID(appURL: string): Promise { 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;