Skip to content

Commit 14504e4

Browse files
authored
V2: embeds and CSP (#2885)
1 parent 4ed7957 commit 14504e4

File tree

17 files changed

+141
-240
lines changed

17 files changed

+141
-240
lines changed

.github/workflows/ci.yaml

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -235,6 +235,7 @@ jobs:
235235
runs-on: ubuntu-latest
236236
name: Visual Testing
237237
needs: deploy
238+
timeout-minutes: 6
238239
steps:
239240
- name: Checkout
240241
uses: actions/checkout@v4
@@ -259,6 +260,7 @@ jobs:
259260
runs-on: ubuntu-latest
260261
name: Visual Testing v2
261262
needs: deploy-v2-vercel
263+
timeout-minutes: 6
262264
steps:
263265
- name: Checkout
264266
uses: actions/checkout@v4
@@ -284,6 +286,7 @@ jobs:
284286
runs-on: ubuntu-latest
285287
name: Visual Testing Customers
286288
needs: deploy
289+
timeout-minutes: 6
287290
steps:
288291
- name: Checkout
289292
uses: actions/checkout@v4
@@ -309,6 +312,7 @@ jobs:
309312
runs-on: ubuntu-latest
310313
name: Visual Testing Customers v2
311314
needs: deploy-v2-vercel
315+
timeout-minutes: 6
312316
steps:
313317
- name: Checkout
314318
uses: actions/checkout@v4
@@ -351,6 +355,7 @@ jobs:
351355
format:
352356
runs-on: ubuntu-latest
353357
name: Format
358+
timeout-minutes: 6
354359
steps:
355360
- name: Checkout
356361
uses: actions/checkout@v4
@@ -364,6 +369,7 @@ jobs:
364369
test:
365370
runs-on: ubuntu-latest
366371
name: Test
372+
timeout-minutes: 6
367373
steps:
368374
- name: Checkout
369375
uses: actions/checkout@v4
@@ -378,6 +384,7 @@ jobs:
378384
# CI to check that the repository builds correctly on a machine without the credentials
379385
runs-on: ubuntu-latest
380386
name: Build (Open Source)
387+
timeout-minutes: 6
381388
env:
382389
NPM_TOKEN_READONLY: ''
383390
steps:
@@ -393,6 +400,7 @@ jobs:
393400
typecheck:
394401
runs-on: ubuntu-latest
395402
name: Typecheck
403+
timeout-minutes: 6
396404
steps:
397405
- name: Checkout
398406
uses: actions/checkout@v4

bun.lock

Lines changed: 0 additions & 93 deletions
Large diffs are not rendered by default.

package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@
77
"turbo": "^2.4.4",
88
"vercel": "^39.3.0"
99
},
10-
"packageManager": "[email protected].3",
10+
"packageManager": "[email protected].4",
1111
"overrides": {
1212
"@codemirror/state": "6.4.1",
1313
"react": "18.3.1",

packages/gitbook-v2/src/lib/data/api.ts

Lines changed: 51 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
import { type ComputedContentSource, GitBookAPI } from '@gitbook/api';
22
import { GITBOOK_API_TOKEN, GITBOOK_API_URL, GITBOOK_USER_AGENT } from '@v2/lib/env';
3-
import { unstable_cacheTag as cacheTag } from 'next/cache';
3+
import { unstable_cacheLife as cacheLife, unstable_cacheTag as cacheTag } from 'next/cache';
44
import {
55
getChangeRequestCacheTag,
66
getHostnameCacheTag,
@@ -118,6 +118,12 @@ export function createDataFetcher(input: DataFetcherInput = commonInput): GitBoo
118118
source: params.source,
119119
});
120120
},
121+
getEmbedByUrl(params) {
122+
return getEmbedByUrl(input, {
123+
url: params.url,
124+
spaceId: params.spaceId,
125+
});
126+
},
121127

122128
//
123129
// API that are not tied to the token
@@ -139,6 +145,8 @@ export function createDataFetcher(input: DataFetcherInput = commonInput): GitBoo
139145
async function getUserById(input: DataFetcherInput, userId: string) {
140146
'use cache';
141147

148+
cacheLife('days');
149+
142150
try {
143151
const res = await getAPI(input).users.getUserById(userId);
144152
return res.data;
@@ -160,6 +168,7 @@ async function getSpace(
160168
) {
161169
'use cache';
162170

171+
cacheLife('days');
163172
cacheTag(getSpaceCacheTag(params.spaceId));
164173

165174
const res = await getAPI(input).spaces.getSpaceById(params.spaceId, {
@@ -177,6 +186,8 @@ async function getChangeRequest(
177186
) {
178187
'use cache';
179188

189+
cacheLife('minutes');
190+
180191
try {
181192
const res = await getAPI(input).spaces.getChangeRequestById(
182193
params.spaceId,
@@ -203,6 +214,8 @@ async function getRevision(
203214
) {
204215
'use cache';
205216

217+
cacheLife('max');
218+
206219
const res = await getAPI(input).spaces.getRevisionById(params.spaceId, params.revisionId, {
207220
metadata: params.metadata,
208221
});
@@ -219,6 +232,8 @@ async function getRevisionPages(
219232
) {
220233
'use cache';
221234

235+
cacheLife('max');
236+
222237
const res = await getAPI(input).spaces.listPagesInRevisionById(
223238
params.spaceId,
224239
params.revisionId,
@@ -239,6 +254,8 @@ async function getRevisionFile(
239254
) {
240255
'use cache';
241256

257+
cacheLife('max');
258+
242259
try {
243260
const res = await getAPI(input).spaces.getFileInRevisionById(
244261
params.spaceId,
@@ -265,8 +282,9 @@ async function getRevisionPageByPath(
265282
) {
266283
'use cache';
267284

268-
const encodedPath = encodeURIComponent(params.path);
285+
cacheLife('max');
269286

287+
const encodedPath = encodeURIComponent(params.path);
270288
try {
271289
const res = await getAPI(input).spaces.getPageInRevisionByPath(
272290
params.spaceId,
@@ -293,6 +311,8 @@ async function getDocument(
293311
) {
294312
'use cache';
295313

314+
cacheLife('max');
315+
296316
const res = await getAPI(input).spaces.getDocumentById(params.spaceId, params.documentId);
297317
return res.data;
298318
}
@@ -306,6 +326,9 @@ async function getComputedDocument(
306326
) {
307327
'use cache';
308328

329+
// TODO: we need to resolve dependencies and pass them in the cache key
330+
cacheLife('days');
331+
309332
const res = await getAPI(input).spaces.getComputedDocument(params.spaceId, {
310333
source: params.source,
311334
});
@@ -322,6 +345,8 @@ async function getReusableContent(
322345
) {
323346
'use cache';
324347

348+
cacheLife('max');
349+
325350
try {
326351
const res = await getAPI(input).spaces.getReusableContentInRevisionById(
327352
params.spaceId,
@@ -348,6 +373,7 @@ async function getLatestOpenAPISpecVersionContent(
348373
'use cache';
349374

350375
cacheTag(getOpenAPISpecCacheTag(params.organizationId, params.slug));
376+
cacheLife('days');
351377

352378
try {
353379
const res = await getAPI(input).orgs.getLatestOpenApiSpecVersionContent(
@@ -378,6 +404,7 @@ async function getPublishedContentByUrl(
378404

379405
const hostname = new URL(url).hostname;
380406
cacheTag(getHostnameCacheTag(hostname));
407+
cacheLife('days');
381408

382409
const res = await getAPI(input).urls.getPublishedContentByUrl({
383410
url,
@@ -401,7 +428,10 @@ async function getPublishedContentSite(
401428
}
402429
) {
403430
'use cache';
431+
404432
cacheTag(getSiteCacheTag(params.siteId));
433+
cacheLife('days');
434+
405435
const res = await getAPI(input).orgs.getPublishedContentSite(
406436
params.organizationId,
407437
params.siteId,
@@ -423,6 +453,9 @@ async function getSiteRedirectBySource(
423453
) {
424454
'use cache';
425455

456+
cacheTag(getSiteCacheTag(params.siteId));
457+
cacheLife('days');
458+
426459
try {
427460
const res = await getAPI(input).orgs.getSiteRedirectBySource(
428461
params.organizationId,
@@ -449,6 +482,22 @@ async function getSiteRedirectBySource(
449482
}
450483
}
451484

485+
async function getEmbedByUrl(
486+
input: DataFetcherInput,
487+
params: {
488+
url: string;
489+
spaceId: string;
490+
}
491+
) {
492+
'use cache';
493+
494+
cacheLife('weeks');
495+
496+
const api = getAPI(input);
497+
const res = await api.spaces.getEmbedByUrlInSpace(params.spaceId, { url: params.url });
498+
return res.data;
499+
}
500+
452501
function getAPI(input: DataFetcherInput) {
453502
const { apiEndpoint, apiToken } = input;
454503
const api = new GitBookAPI({

packages/gitbook-v2/src/lib/data/types.ts

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -121,4 +121,9 @@ export interface GitBookDataFetcher {
121121
siteShareKey: string | undefined;
122122
source: string;
123123
}): Promise<{ redirect: api.SiteRedirect | null; target: string } | null>;
124+
125+
/**
126+
* Get an embed by its URL.
127+
*/
128+
getEmbedByUrl(params: { url: string; spaceId: string }): Promise<api.Embed>;
124129
}

packages/gitbook-v2/src/lib/env.ts

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -36,3 +36,9 @@ export const GITBOOK_USER_AGENT = process.env.GITBOOK_USER_AGENT ?? 'GitBook-Ope
3636
export const GITBOOK_DISABLE_TRACKING = Boolean(
3737
!!process.env.GITBOOK_DISABLE_TRACKING || process.env.NODE_ENV !== 'production'
3838
);
39+
40+
/**
41+
* Hostname serving the integrations.
42+
*/
43+
export const GITBOOK_INTEGRATIONS_HOST =
44+
process.env.GITBOOK_INTEGRATIONS_HOST ?? 'integrations.gitbook.com';

packages/gitbook-v2/src/middleware.ts

Lines changed: 31 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,8 @@
11
import { GitBookAPIError } from '@gitbook/api';
2-
import { NextResponse } from 'next/server';
32
import type { NextRequest } from 'next/server';
3+
import { NextResponse } from 'next/server';
44

5+
import { getContentSecurityPolicy } from '@/lib/csp';
56
import { removeTrailingSlash } from '@/lib/paths';
67
import { getPublishedContentByURL } from '@v2/lib/data';
78
import { MiddlewareHeaders } from '@v2/lib/middleware';
@@ -13,12 +14,16 @@ export const config = {
1314
type URLWithMode = { url: URL; mode: 'url' | 'url-host' };
1415

1516
export async function middleware(request: NextRequest) {
16-
const extracted = extractURL(request);
17-
if (extracted) {
18-
return serveSiteByURL(request, extracted);
19-
}
17+
try {
18+
const extracted = extractURL(request);
19+
if (extracted) {
20+
return serveSiteByURL(request, extracted);
21+
}
2022

21-
return NextResponse.next();
23+
return NextResponse.next();
24+
} catch (error) {
25+
return serveErrorResponse(error as Error);
26+
}
2227
}
2328

2429
/**
@@ -35,14 +40,6 @@ async function serveSiteByURL(request: NextRequest, urlWithMode: URLWithMode) {
3540
});
3641

3742
if (result.error) {
38-
if (result.error instanceof GitBookAPIError) {
39-
return NextResponse.json(
40-
{
41-
error: result.error.message,
42-
},
43-
{ status: result.error.code }
44-
);
45-
}
4643
throw result.error;
4744
}
4845

@@ -68,9 +65,28 @@ async function serveSiteByURL(request: NextRequest, urlWithMode: URLWithMode) {
6865
encodeURIComponent(removeTrailingSlash(data.pathname) || '/'),
6966
].join('/');
7067

71-
return NextResponse.rewrite(new URL(`/${route}`, request.url), {
68+
const response = NextResponse.rewrite(new URL(`/${route}`, request.url), {
7269
headers: requestHeaders,
7370
});
71+
72+
// Add Content Security Policy header
73+
response.headers.set('content-security-policy', getContentSecurityPolicy());
74+
75+
return response;
76+
}
77+
78+
/**
79+
* Serve an error response.
80+
*/
81+
function serveErrorResponse(error: Error) {
82+
if (error instanceof GitBookAPIError) {
83+
return NextResponse.json(
84+
{ error: error.message },
85+
{ status: 500, headers: { 'content-type': 'application/json' } }
86+
);
87+
}
88+
89+
throw error;
7490
}
7591

7692
/**

packages/gitbook/package.json

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -37,7 +37,6 @@
3737
"assert-never": "^1.2.1",
3838
"bun-types": "^1.1.20",
3939
"classnames": "^2.5.1",
40-
"content-security-policy-merger": "^1.0.0",
4140
"framer-motion": "^10.16.14",
4241
"js-cookie": "^3.0.5",
4342
"jsontoxml": "^1.0.1",

packages/gitbook/src/app/middleware/(site)/(content)/layout.tsx

Lines changed: 0 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,6 @@ import {
77
generateSiteLayoutMetadata,
88
generateSiteLayoutViewport,
99
} from '@/components/SiteLayout';
10-
import { getContentSecurityPolicyNonce } from '@/lib/csp';
1110
import { getSiteContentPointer } from '@/lib/pointer';
1211
import { shouldTrackEvents } from '@/lib/tracking';
1312
import { fetchV1ContextForSitePointer } from '@/lib/v1';
@@ -21,14 +20,12 @@ export const dynamic = 'force-dynamic';
2120
export default async function ContentLayout(props: { children: React.ReactNode }) {
2221
const { children } = props;
2322

24-
const nonce = await getContentSecurityPolicyNonce();
2523
const context = await fetchLayoutData();
2624
const queryStringTheme = await getThemeFromMiddleware();
2725

2826
return (
2927
<SiteLayout
3028
context={context}
31-
nonce={nonce}
3229
forcedTheme={queryStringTheme}
3330
withTracking={await shouldTrackEvents()}
3431
>

0 commit comments

Comments
 (0)