Compare commits

..

1 Commits

Author SHA1 Message Date
Nolann Biron 010b838d84 test 2026-03-11 14:46:16 +01:00
65 changed files with 404 additions and 1153 deletions
+5
View File
@@ -0,0 +1,5 @@
---
"@gitbook/embed": minor
---
Support `assistantName` property to override Assistant name
+5
View File
@@ -0,0 +1,5 @@
---
"gitbook": patch
---
Add backward compatibility for OpenAPI spec content URL
-5
View File
@@ -1,5 +0,0 @@
---
"gitbook": patch
---
Fix Variant dropdown not showing in GitBook embed
+6
View File
@@ -0,0 +1,6 @@
---
"@gitbook/openapi-parser": patch
"@gitbook/react-openapi": patch
---
Support x-enable-proxy at operation level
-5
View File
@@ -1,5 +0,0 @@
---
"gitbook": patch
---
Use localized titles/descriptions for site entities
-5
View File
@@ -1,5 +0,0 @@
---
"gitbook": patch
---
Import site redirect patterns from @gitbook/api
+5
View File
@@ -0,0 +1,5 @@
---
"gitbook": patch
---
Refactor embeddable context to merge local & site properties in one unified way
+6
View File
@@ -0,0 +1,6 @@
---
"@gitbook/react-openapi": patch
"gitbook": patch
---
Move ~scalar/proxy route to site route group
+6
View File
@@ -0,0 +1,6 @@
---
"@gitbook/react-openapi": patch
"gitbook": patch
---
Add token verification on OpenAPI proxy
-5
View File
@@ -1,5 +0,0 @@
---
"@gitbook/react-openapi": patch
---
Support x-gitbook-token-placeholder as TryIt prefill fallback for HTTP auth (basic and bearer)
+5
View File
@@ -0,0 +1,5 @@
---
"gitbook": patch
---
Add support for localized site section titles
+5
View File
@@ -136,6 +136,7 @@ jobs:
env:
BASE_URL: ${{ needs.deploy-v2-vercel.outputs.deployment-url }}
SITE_BASE_URL: ${{ needs.deploy-v2-vercel.outputs.deployment-url }}/url/
SITE_PREVIEW_BASE_URL: ${{ needs.deploy-v2-vercel.outputs.deployment-url }}/preview/
ARGOS_TOKEN: ${{ secrets.ARGOS_TOKEN }}
ARGOS_BUILD_NAME: v2-vercel
visual-testing-v2-cloudflare:
@@ -160,6 +161,7 @@ jobs:
env:
BASE_URL: ${{ needs.deploy-v2-cloudflare.outputs.deployment-url }}
SITE_BASE_URL: ${{ needs.deploy-v2-cloudflare.outputs.deployment-url }}/url/
SITE_PREVIEW_BASE_URL: ${{ needs.deploy-v2-cloudflare.outputs.deployment-url }}/preview/
ARGOS_TOKEN: ${{ secrets.ARGOS_TOKEN }}
ARGOS_BUILD_NAME: v2-cloudflare
visual-testing-customers-v2:
@@ -184,6 +186,7 @@ jobs:
env:
BASE_URL: ${{ needs.deploy-v2-vercel.outputs.deployment-url }}
SITE_BASE_URL: ${{ needs.deploy-v2-vercel.outputs.deployment-url }}/url/
SITE_PREVIEW_BASE_URL: ${{ needs.deploy-v2-vercel.outputs.deployment-url }}/preview/
ARGOS_TOKEN: ${{ secrets.ARGOS_TOKEN }}
ARGOS_BUILD_NAME: customers-v2-vercel
visual-testing-customers-v2-cloudflare:
@@ -208,6 +211,7 @@ jobs:
env:
BASE_URL: ${{ needs.deploy-v2-cloudflare.outputs.deployment-url }}
SITE_BASE_URL: ${{ needs.deploy-v2-cloudflare.outputs.deployment-url }}/url/
SITE_PREVIEW_BASE_URL: ${{ needs.deploy-v2-cloudflare.outputs.deployment-url }}/preview/
ARGOS_TOKEN: ${{ secrets.ARGOS_TOKEN }}
ARGOS_BUILD_NAME: customers-v2-cloudflare
browserless-testing-v2-vercel:
@@ -231,6 +235,7 @@ jobs:
env:
BASE_URL: ${{needs.deploy-v2-vercel.outputs.deployment-url}}
SITE_BASE_URL: ${{ needs.deploy-v2-vercel.outputs.deployment-url }}/url/
SITE_PREVIEW_BASE_URL: ${{ needs.deploy-v2-vercel.outputs.deployment-url }}/preview/
# browserless-testing-v2-cloudflare:
# runs-on: ubuntu-latest
# name: Browserless Testing v2 (Cloudflare)
+39 -40
View File
@@ -7,8 +7,8 @@
"devDependencies": {
"@biomejs/biome": "^1.9.4",
"@changesets/cli": "^2.30.0",
"turbo": "^2.8.16",
"vercel": "^50.31.1",
"turbo": "^2.8.13",
"vercel": "^50.26.1",
},
},
"packages/browser-types": {
@@ -48,7 +48,7 @@
},
"packages/embed": {
"name": "@gitbook/embed",
"version": "0.4.0",
"version": "0.3.0",
"dependencies": {
"@gitbook/api": "catalog:",
"@gitbook/icons": "workspace:",
@@ -109,7 +109,7 @@
},
"packages/gitbook": {
"name": "gitbook",
"version": "0.22.3",
"version": "0.22.2",
"dependencies": {
"@cloudflare/workers-types": "^4.20251011.0",
"@gitbook/api": "catalog:",
@@ -171,7 +171,6 @@
"react": "catalog:",
"react-dom": "catalog:",
"react-hotkeys-hook": "^4.4.1",
"rehype-raw": "^7.0.0",
"rehype-sanitize": "^6.0.0",
"rehype-stringify": "^10.0.1",
"remark-gfm": "^4.0.1",
@@ -245,7 +244,7 @@
},
"packages/openapi-parser": {
"name": "@gitbook/openapi-parser",
"version": "3.0.10",
"version": "3.0.9",
"dependencies": {
"@scalar/json-magic": "^0.9.4",
"@scalar/openapi-parser": "^0.24.5",
@@ -304,7 +303,7 @@
},
"packages/react-openapi": {
"name": "@gitbook/react-openapi",
"version": "1.5.10",
"version": "1.5.9",
"dependencies": {
"@gitbook/expr": "workspace:*",
"@gitbook/openapi-parser": "workspace:*",
@@ -349,7 +348,7 @@
"react-dom": "catalog:",
},
"catalog": {
"@gitbook/api": "0.169.0",
"@gitbook/api": "0.167.0",
"@scalar/api-client-react": "^1.3.46",
"@tsconfig/node20": "^20.1.6",
"@tsconfig/strictest": "^2.0.6",
@@ -746,7 +745,7 @@
"@fortawesome/fontawesome-svg-core": ["@fortawesome/fontawesome-svg-core@7.1.0", "", { "dependencies": { "@fortawesome/fontawesome-common-types": "7.1.0" } }, "sha512-fNxRUk1KhjSbnbuBxlWSnBLKLBNun52ZBTcs22H/xEEzM6Ap81ZFTQ4bZBxVQGQgVY0xugKGoRcCbaKjLQ3XZA=="],
"@gitbook/api": ["@gitbook/api@0.169.0", "", { "dependencies": { "event-iterator": "^2.0.0", "eventsource-parser": "^3.0.0" } }, "sha512-kzJT8P5HCnqeYG3kFmBfvKMjQhTuUVmNV5Wvyh87QnoOA3odwh2C3qONpTmBA6cNE3ymDnxs8SJT9FG/qquRZQ=="],
"@gitbook/api": ["@gitbook/api@0.167.0", "", { "dependencies": { "event-iterator": "^2.0.0", "eventsource-parser": "^3.0.0" } }, "sha512-9A9Bsgi2aRbGf3DPejK4YQh9M/+grtNymFwUkK3B3N4m7VOxcUMQ2HaV7s51mtrBdWKcEdeFm4LEnEaAGHcM1A=="],
"@gitbook/browser-types": ["@gitbook/browser-types@workspace:packages/browser-types"],
@@ -1702,63 +1701,63 @@
"@ungap/structured-clone": ["@ungap/structured-clone@1.2.0", "", {}, "sha512-zuVdFrMJiuCDQUMCzQaD6KL28MjnqqN8XnAqiEq9PNm/hCPTSGfrXCOfwj1ow4LFb/tNymJPwsNbVePc1xFqrQ=="],
"@vercel/backends": ["@vercel/backends@0.0.45", "", { "dependencies": { "@vercel/build-utils": "13.8.0", "@vercel/nft": "1.3.0", "execa": "3.2.0", "fs-extra": "11.1.0", "oxc-transform": "0.111.0", "path-to-regexp": "8.3.0", "resolve.exports": "2.0.3", "rolldown": "1.0.0-rc.1", "srvx": "0.8.9", "tsx": "4.21.0", "zod": "3.22.4" }, "peerDependencies": { "typescript": "^4.0.0 || ^5.0.0" } }, "sha512-KIdt/z4LfH7NgFMqgSuKi0H9UIasly7ByzP+/ZXulgNrWyeJKT9KCas3SDT65o5tU6x1D/jBysZA9AnOt8Ivew=="],
"@vercel/backends": ["@vercel/backends@0.0.40", "", { "dependencies": { "@vercel/build-utils": "13.6.2", "@vercel/nft": "1.3.0", "execa": "3.2.0", "fs-extra": "11.1.0", "oxc-transform": "0.111.0", "path-to-regexp": "8.3.0", "resolve.exports": "2.0.3", "rolldown": "1.0.0-rc.1", "srvx": "0.8.9", "tsx": "4.21.0", "zod": "3.22.4" }, "peerDependencies": { "typescript": "^4.0.0 || ^5.0.0" } }, "sha512-cYyfPeGrpqyxBgcgKoaZJ+9M7NRKf1eevzKkrX9QRmiBBlGduR34hV859uK4ouBJByHE7n9Tkga2dRLOH8pMcQ=="],
"@vercel/blob": ["@vercel/blob@2.3.0", "", { "dependencies": { "async-retry": "^1.3.3", "is-buffer": "^2.0.5", "is-node-process": "^1.2.0", "throttleit": "^2.1.0", "undici": "^6.23.0" } }, "sha512-oYWiJbWRQ7gz9Mj0X/NHFJ3OcLMOBzq/2b3j6zeNrQmtFo6dHwU8FAwNpxVIYddVMd+g8eqEi7iRueYx8FtM0Q=="],
"@vercel/build-utils": ["@vercel/build-utils@13.8.0", "", { "dependencies": { "@vercel/python-analysis": "0.9.1" } }, "sha512-moQS4Qd0pvluPd6WRTHxLN3Hh0oSObVNdFv3V0spiEmCk/wm6571up3n1th2PQFqf1a3gheNfxzL7h4I9CWs2A=="],
"@vercel/build-utils": ["@vercel/build-utils@13.6.2", "", { "dependencies": { "@vercel/python-analysis": "0.8.1" } }, "sha512-KiwRUd2x1ZHm73p+/hfdVWZHWXT8r+oN/5ZN1lJyb41AGK4Zhrug6X60jNyVhnw6GjpwBuPTBcys29cCsPjkWQ=="],
"@vercel/cervel": ["@vercel/cervel@0.0.32", "", { "dependencies": { "@vercel/backends": "0.0.45" }, "peerDependencies": { "typescript": "^4.0.0 || ^5.0.0" }, "bin": { "cervel": "bin/cervel.mjs" } }, "sha512-g/LIa97d/m3yIZGyBjwl4FOQK1Lg5HU2+N3uHiGVajLoSgJM2aBiwETDZc8bVCr8IO3IqXZQsT2hGy1Jnyko1g=="],
"@vercel/cervel": ["@vercel/cervel@0.0.27", "", { "dependencies": { "@vercel/backends": "0.0.40" }, "peerDependencies": { "typescript": "^4.0.0 || ^5.0.0" }, "bin": { "cervel": "bin/cervel.mjs" } }, "sha512-w6FPgnnZD2nr43pkhXSY0tGMwCJT6kh8f8VoEL97ISSkxywZYX+Bw5FxrNavAEMC0wYm7esEosOdAEAh8Twbag=="],
"@vercel/detect-agent": ["@vercel/detect-agent@1.2.0", "", {}, "sha512-kY0+TPdYr170bVmVSCg0XvwakWQMENy79uf/sM86DF/HVabaagdTjPwSPujlZid6Bgr66R4kHhjg4bkOxEzFDA=="],
"@vercel/detect-agent": ["@vercel/detect-agent@1.1.0", "", {}, "sha512-Zfq6FbIcYl9gaAmVu6ROsqUiCNwpEj3Ljz/tMX5fl12Z95OFOxzf7vlO03WE5JBU/ri1tBDFHnW41dihMINOPQ=="],
"@vercel/elysia": ["@vercel/elysia@0.1.48", "", { "dependencies": { "@vercel/node": "5.6.15", "@vercel/static-config": "3.2.0" } }, "sha512-QlmOHUSOx/uE67Y6u/o8VbNF7ebZ8SZFbrEoBo7iZAd0MC2Vn4xUmlrHFXNR/FEkHFXWkhFKda5Ade3XMqMY8A=="],
"@vercel/elysia": ["@vercel/elysia@0.1.43", "", { "dependencies": { "@vercel/node": "5.6.10", "@vercel/static-config": "3.1.2" } }, "sha512-Q5XCGVMuO0XF2n/XEEP43d+AIk8Yl09NwJmJ0m9w5Mx0Pyacb1Yo6dcbn6vnsj4j50dpYGeejdaqu77xOKi8mQ=="],
"@vercel/error-utils": ["@vercel/error-utils@2.0.3", "", {}, "sha512-CqC01WZxbLUxoiVdh9B/poPbNpY9U+tO1N9oWHwTl5YAZxcqXmmWJ8KNMFItJCUUWdY3J3xv8LvAuQv2KZ5YdQ=="],
"@vercel/express": ["@vercel/express@0.1.57", "", { "dependencies": { "@vercel/cervel": "0.0.32", "@vercel/nft": "1.1.1", "@vercel/node": "5.6.15", "@vercel/static-config": "3.2.0", "fs-extra": "11.1.0", "path-to-regexp": "8.3.0", "ts-morph": "12.0.0", "zod": "3.22.4" } }, "sha512-/Ih1eiJrBGSW6JPzexB8y8AMX/8MzuDUf0xzmpYbKLCbehp3NNuxde5RxT+6cALglFivDETsJBCFwaFuIh3ZqQ=="],
"@vercel/express": ["@vercel/express@0.1.52", "", { "dependencies": { "@vercel/cervel": "0.0.27", "@vercel/nft": "1.1.1", "@vercel/node": "5.6.10", "@vercel/static-config": "3.1.2", "fs-extra": "11.1.0", "path-to-regexp": "8.3.0", "ts-morph": "12.0.0", "zod": "3.22.4" } }, "sha512-lvZyw/7IOVMx6YqniijlMrU0ys+bcbqkY+5slGQMoXrtW7Mo2RhF9f27y+1Bl1ELv8JzyrXhOLHu2f5gn4CsgQ=="],
"@vercel/fastify": ["@vercel/fastify@0.1.51", "", { "dependencies": { "@vercel/node": "5.6.15", "@vercel/static-config": "3.2.0" } }, "sha512-c9CwFQqmoUm5eEGwAcqb98DcxsRmSZGngCAo1B+nlDhaACKcrJ7Ro0tYdTDhi8Lkt+OcPnRWr4+pepgWwH94GQ=="],
"@vercel/fastify": ["@vercel/fastify@0.1.46", "", { "dependencies": { "@vercel/node": "5.6.10", "@vercel/static-config": "3.1.2" } }, "sha512-R8uSV3SSbFJ39o3VAfsKwlWOPGDfQyNXxJ2dT8Z05GQ4O3qgGTcRd/oEothQZgnJR9fjGzDQ3q7SwWBd12150A=="],
"@vercel/fun": ["@vercel/fun@1.3.0", "", { "dependencies": { "@tootallnate/once": "2.0.0", "async-listen": "1.2.0", "debug": "4.3.4", "generic-pool": "3.4.2", "micro": "9.3.5-canary.3", "ms": "2.1.1", "node-fetch": "2.6.7", "path-to-regexp": "8.2.0", "promisepipe": "3.0.0", "semver": "7.5.4", "stat-mode": "0.3.0", "stream-to-promise": "2.2.0", "tar": "7.5.7", "tinyexec": "0.3.2", "tree-kill": "1.2.2", "uid-promise": "1.0.0", "xdg-app-paths": "5.1.0", "yauzl-promise": "2.1.3" } }, "sha512-8erw9uPe0dFg45THkNxmjtvMX143SkZebmjgSVbcM3XCkXu3RIiBaJMcMNG8aaS+rnTuw8+d4De9HVT0M/r3wg=="],
"@vercel/gatsby-plugin-vercel-analytics": ["@vercel/gatsby-plugin-vercel-analytics@1.0.11", "", { "dependencies": { "web-vitals": "0.2.4" } }, "sha512-iTEA0vY6RBPuEzkwUTVzSHDATo1aF6bdLLspI68mQ/BTbi5UQEGjpjyzdKOVcSYApDtFU6M6vypZ1t4vIEnHvw=="],
"@vercel/gatsby-plugin-vercel-builder": ["@vercel/gatsby-plugin-vercel-builder@2.1.0", "", { "dependencies": { "@sinclair/typebox": "0.25.24", "@vercel/build-utils": "13.8.0", "esbuild": "0.27.0", "etag": "1.8.1", "fs-extra": "11.1.0" } }, "sha512-avJ5IFev2h2K6E/Pd7qd00cFLALj3OyEmQE3UoGs1dmoncINFqa1RoIZDJ9wIhWm1Euan4wrFMRsXkCwNhEGhw=="],
"@vercel/gatsby-plugin-vercel-builder": ["@vercel/gatsby-plugin-vercel-builder@2.0.142", "", { "dependencies": { "@sinclair/typebox": "0.25.24", "@vercel/build-utils": "13.6.2", "esbuild": "0.27.0", "etag": "1.8.1", "fs-extra": "11.1.0" } }, "sha512-F6Mo5fROP57wX/vAngXHA+xBZVwi98vU5YT1J+n/PYxIwbOVCa6SyPOI98o6a+uLQnchqbWbcXcV2CaYmU85VA=="],
"@vercel/go": ["@vercel/go@3.4.4", "", {}, "sha512-jfIx/gUe2YAp46/H4uqdA+PLQQuRwb5lwU37m/e5wrnMCUZ2ksuhg50xPHcBBweboYeE6WuT4e1W6Cwp3tBPzg=="],
"@vercel/go": ["@vercel/go@3.4.3", "", {}, "sha512-LDT4wpx7SW2UJHd3rOL4+2m2V4w7a5zjyuU0pu0yDBiuxp9bEh7QuAH5qZC7pINGLC3vGftiFVzynd5N454sVA=="],
"@vercel/h3": ["@vercel/h3@0.1.57", "", { "dependencies": { "@vercel/node": "5.6.15", "@vercel/static-config": "3.2.0" } }, "sha512-I7Q1ity7xEdIVw4OQuKOAR8i6DrvfjpKy+y2uTwbqBD80Gg0KpsMS/KKA+UjvCuclFmP/EHYS3kpMLc/O1rVqg=="],
"@vercel/h3": ["@vercel/h3@0.1.52", "", { "dependencies": { "@vercel/node": "5.6.10", "@vercel/static-config": "3.1.2" } }, "sha512-bk8id7w9evxD+JeOUJt6qfDohGwJFBVh7N1qh1OG/nngzzb/Ojg+poH5EGpYwYiKymPK7NMgQl0fb0ZjhIU4bg=="],
"@vercel/hono": ["@vercel/hono@0.2.51", "", { "dependencies": { "@vercel/nft": "1.1.1", "@vercel/node": "5.6.15", "@vercel/static-config": "3.2.0", "fs-extra": "11.1.0", "path-to-regexp": "8.3.0", "ts-morph": "12.0.0", "zod": "3.22.4" } }, "sha512-iYEjjF4qR3gTZpVoB4sMQNm5OOdKw5/P2bL1CtolDTeGaea9KwqUrLyXmdK4N41HIRZr/wduID31mmHhSdC3sA=="],
"@vercel/hono": ["@vercel/hono@0.2.46", "", { "dependencies": { "@vercel/nft": "1.1.1", "@vercel/node": "5.6.10", "@vercel/static-config": "3.1.2", "fs-extra": "11.1.0", "path-to-regexp": "8.3.0", "ts-morph": "12.0.0", "zod": "3.22.4" } }, "sha512-rg+6PnvIV20lUM3r2fcjJ/1g1z3ND6h3EqlOQytwCuwpP/jB88TUy+guutB4rDpylPvHzleZqjgi2rE9yH4URg=="],
"@vercel/hydrogen": ["@vercel/hydrogen@1.3.6", "", { "dependencies": { "@vercel/static-config": "3.2.0", "ts-morph": "12.0.0" } }, "sha512-Ec8dKEjGIM4BfThcRLtQs5zaJ4+iJbgLZwkytwi7Blk8VrK6W2F1dtLDmVQYZdVnQcnmHmTx8mxUuMkfP06Mnw=="],
"@vercel/hydrogen": ["@vercel/hydrogen@1.3.5", "", { "dependencies": { "@vercel/static-config": "3.1.2", "ts-morph": "12.0.0" } }, "sha512-7EE6yVKcCnjMb1io9y069GkLyGyIzRbW3Krm3Q7EEfJ3P46h9xe9v/O5UhBoPrwtqDUHxmDngZp9YyfgY8IITA=="],
"@vercel/koa": ["@vercel/koa@0.1.31", "", { "dependencies": { "@vercel/node": "5.6.15", "@vercel/static-config": "3.2.0" } }, "sha512-Gj4sjqNA80/gnHpC0tRPCTtVEct69tUK21fsjVmawCa+nDNQ4bqXu3dSewna7GtCcp9KnrWgzAIaeVAQUl53mQ=="],
"@vercel/koa": ["@vercel/koa@0.1.26", "", { "dependencies": { "@vercel/node": "5.6.10", "@vercel/static-config": "3.1.2" } }, "sha512-TzH4xovhGbr2kmZyvMraaJ3A28qRdo8SxkBO+LIb3ahM739VyTAkH3EYR5CPaemkRv00+nTyBxJJxfnhP1n2MA=="],
"@vercel/nestjs": ["@vercel/nestjs@0.2.52", "", { "dependencies": { "@vercel/node": "5.6.15", "@vercel/static-config": "3.2.0" } }, "sha512-yfy4rpWJ1BRWZ1xBBPew0egVIblFe6G7laCKDzyESnbw19zY0F16g9HCu1H/0IfuKIQvOc95dtbmBsqLm9jyqw=="],
"@vercel/nestjs": ["@vercel/nestjs@0.2.47", "", { "dependencies": { "@vercel/node": "5.6.10", "@vercel/static-config": "3.1.2" } }, "sha512-/yIbLc5rv8GQ+lsJ45JkObcNiMe5yUgXx2d8wryW1SajPV1/iP+wFRK28/Q+6zdvapcgqhFRwInZh+61um/ZWg=="],
"@vercel/next": ["@vercel/next@4.16.1", "", { "dependencies": { "@vercel/nft": "1.1.1" } }, "sha512-gwy3XQRZ/f6RdKuC7BZIRMAzUOQf/R5+k9LMf1LcOm1CVZOLpDhDkeZMTG5vb3Lk9LWwBrJJ2ohhZaYuYEOHaw=="],
"@vercel/next": ["@vercel/next@4.15.39", "", { "dependencies": { "@vercel/nft": "1.1.1" } }, "sha512-E3Wnn5Wfxa7e2RvguE4YNO8OBoOVZhnRXDRfn5jJ9ct+GVRZDgPS9VdXK3eQazMJUCLmQCKdyTca/I57ImPNpQ=="],
"@vercel/nft": ["@vercel/nft@1.3.0", "", { "dependencies": { "@mapbox/node-pre-gyp": "^2.0.0", "@rollup/pluginutils": "^5.1.3", "acorn": "^8.6.0", "acorn-import-attributes": "^1.9.5", "async-sema": "^3.1.1", "bindings": "^1.4.0", "estree-walker": "2.0.2", "glob": "^13.0.0", "graceful-fs": "^4.2.9", "node-gyp-build": "^4.2.2", "picomatch": "^4.0.2", "resolve-from": "^5.0.0" }, "bin": { "nft": "out/cli.js" } }, "sha512-i4EYGkCsIjzu4vorDUbqglZc5eFtQI2syHb++9ZUDm6TU4edVywGpVnYDein35x9sevONOn9/UabfQXuNXtuzQ=="],
"@vercel/node": ["@vercel/node@5.6.15", "", { "dependencies": { "@edge-runtime/node-utils": "2.3.0", "@edge-runtime/primitives": "4.1.0", "@edge-runtime/vm": "3.2.0", "@types/node": "20.11.0", "@vercel/build-utils": "13.8.0", "@vercel/error-utils": "2.0.3", "@vercel/nft": "1.1.1", "@vercel/static-config": "3.2.0", "async-listen": "3.0.0", "cjs-module-lexer": "1.2.3", "edge-runtime": "2.5.9", "es-module-lexer": "1.4.1", "esbuild": "0.27.0", "etag": "1.8.1", "mime-types": "2.1.35", "node-fetch": "2.6.9", "path-to-regexp": "6.1.0", "path-to-regexp-updated": "npm:path-to-regexp@6.3.0", "ts-morph": "12.0.0", "tsx": "4.21.0", "typescript": "npm:typescript@5.9.3", "undici": "5.28.4" } }, "sha512-xc5fxmdk8jtuUY8y9/8W5UhTn8R1Ii1Fb3q+V8Zv+2moU9enrrBADA9ercHgE0/DtoiNDpb9Wmvnrb4bUcFOzA=="],
"@vercel/node": ["@vercel/node@5.6.10", "", { "dependencies": { "@edge-runtime/node-utils": "2.3.0", "@edge-runtime/primitives": "4.1.0", "@edge-runtime/vm": "3.2.0", "@types/node": "20.11.0", "@vercel/build-utils": "13.6.2", "@vercel/error-utils": "2.0.3", "@vercel/nft": "1.1.1", "@vercel/static-config": "3.1.2", "async-listen": "3.0.0", "cjs-module-lexer": "1.2.3", "edge-runtime": "2.5.9", "es-module-lexer": "1.4.1", "esbuild": "0.27.0", "etag": "1.8.1", "mime-types": "2.1.35", "node-fetch": "2.6.9", "path-to-regexp": "6.1.0", "path-to-regexp-updated": "npm:path-to-regexp@6.3.0", "ts-morph": "12.0.0", "tsx": "4.21.0", "typescript": "npm:typescript@5.9.3", "undici": "5.28.4" } }, "sha512-HEydd6y0KPjCyI9sM3n2/XCjS/fZV/7sO7jFjia9I5XbEJ6LCwkI6rR+GQB751NL0He0vFusy+mm394hJC/Rqg=="],
"@vercel/python": ["@vercel/python@6.22.0", "", { "dependencies": { "@vercel/python-analysis": "0.9.1" } }, "sha512-wyHIWKXrDp+VkiWTIkz/++m037lwIy1PUNLRooYFUUBylBWpF/oYy42Kj8vyAKtsXO+14SBtTCekLE+9+2wmhA=="],
"@vercel/python": ["@vercel/python@6.20.1", "", { "dependencies": { "@vercel/python-analysis": "0.8.1" } }, "sha512-a/VgUJ/N7SfKgE2fen6iaNow/nWEGEQj57SNyPwo4emvPfO1dPxUlZWyAEdctfXYKjnJR2WAU2kYcCDw+zenZg=="],
"@vercel/python-analysis": ["@vercel/python-analysis@0.9.1", "", { "dependencies": { "@bytecodealliance/preview2-shim": "0.17.6", "@renovatebot/pep440": "4.2.1", "fs-extra": "11.1.1", "js-yaml": "4.1.1", "minimatch": "10.1.1", "pip-requirements-js": "1.0.3", "smol-toml": "1.5.2", "zod": "3.22.4" } }, "sha512-ZwEi/F2DPxFPYmfjHFy7qM3+JTWRxD1EMpbIotNNhUyd/pnIG0wNt7S73RJSx62n1Y7pmFOFowoImnhULQgKvA=="],
"@vercel/python-analysis": ["@vercel/python-analysis@0.8.1", "", { "dependencies": { "@bytecodealliance/preview2-shim": "0.17.6", "@renovatebot/pep440": "4.2.1", "fs-extra": "11.1.1", "js-yaml": "4.1.1", "minimatch": "10.1.1", "pip-requirements-js": "1.0.2", "smol-toml": "1.5.2", "zod": "3.22.4" } }, "sha512-gW1pZDqJaTcjZYPvNhXXLOPgLu6vJW9PKweJoX2f8EKAoW+JIiYncl8AddcSlngNhQRG7SqUl2u3qosZM4kUBA=="],
"@vercel/redwood": ["@vercel/redwood@2.4.10", "", { "dependencies": { "@vercel/nft": "1.1.1", "@vercel/static-config": "3.2.0", "semver": "6.3.1", "ts-morph": "12.0.0" } }, "sha512-7C5lUn9g9kLm1KpX55b8iizVPOB6087+kVyQyKyXGk8bbkYySL26yb+LIwyL/7mXwHlq/JTC0AxVdC3nNmPZuw=="],
"@vercel/redwood": ["@vercel/redwood@2.4.9", "", { "dependencies": { "@vercel/nft": "1.1.1", "@vercel/static-config": "3.1.2", "semver": "6.3.1", "ts-morph": "12.0.0" } }, "sha512-U7bYIuWfMEFMIcKKbX7lTT8pFNjig9Q3vLeCYRYQUrKVP8xLoUBXSEfW3ijtWJBUV8GmbZCDI30A16uUfNhN+g=="],
"@vercel/remix-builder": ["@vercel/remix-builder@5.7.0", "", { "dependencies": { "@vercel/error-utils": "2.0.3", "@vercel/nft": "1.1.1", "@vercel/static-config": "3.2.0", "path-to-regexp": "6.1.0", "path-to-regexp-updated": "npm:path-to-regexp@6.3.0", "ts-morph": "12.0.0" } }, "sha512-R44EHl+PQjX5PrCmyGust+bk+65eT5omxOLhEIJkSI90Kx/vvuyKnFNic/Zwo//GCMjxt9httvQUr/6vIBbjHg=="],
"@vercel/remix-builder": ["@vercel/remix-builder@5.6.0", "", { "dependencies": { "@vercel/error-utils": "2.0.3", "@vercel/nft": "1.1.1", "@vercel/static-config": "3.1.2", "path-to-regexp": "6.1.0", "path-to-regexp-updated": "npm:path-to-regexp@6.3.0", "ts-morph": "12.0.0" } }, "sha512-neTpO4aGksYcPJjTbAEUhWmsOdFqgx02H47RUsXBKHdMTW4ZKrL9oAKT3pD+Bv9kUgj7uRzqAr8JeiPILPWybg=="],
"@vercel/ruby": ["@vercel/ruby@2.3.2", "", {}, "sha512-okIgMmPEePyDR9TZYaKM4oftcxVHM5Dbdl7V/tIdh3lq8MGLi7HR5vvQglmZUwZOeovE6MVtezxl960EOzeIiQ=="],
"@vercel/rust": ["@vercel/rust@1.0.5", "", { "dependencies": { "@iarna/toml": "^2.2.5", "execa": "5" } }, "sha512-Y03g59nv1uT6Da+PvB/50WqJSHlaFZ9MSkG00R82dUcTySslMbQdOeaXymZtabrmU8zQYhWDb1/CwBki8sWnaQ=="],
"@vercel/static-build": ["@vercel/static-build@2.9.0", "", { "dependencies": { "@vercel/gatsby-plugin-vercel-analytics": "1.0.11", "@vercel/gatsby-plugin-vercel-builder": "2.1.0", "@vercel/static-config": "3.2.0", "ts-morph": "12.0.0" } }, "sha512-3SHWntz8swxL6ve750dY8kyl4NwVUplYtun/ei7y11q2UnI70WnWmm6L9fi02Wy1o3RGhbvOnlFLcHOUBm3DXQ=="],
"@vercel/static-build": ["@vercel/static-build@2.8.44", "", { "dependencies": { "@vercel/gatsby-plugin-vercel-analytics": "1.0.11", "@vercel/gatsby-plugin-vercel-builder": "2.0.142", "@vercel/static-config": "3.1.2", "ts-morph": "12.0.0" } }, "sha512-xzRBFm+tgVoyE/qSF+BeKXKckLVYXR2dGHsp+fNRttLXi7fq2AUCTM7DNNT4TkkTxcPX5FaJ1tRHxlqLizv91A=="],
"@vercel/static-config": ["@vercel/static-config@3.2.0", "", { "dependencies": { "ajv": "8.6.3", "json-schema-to-ts": "1.6.4", "ts-morph": "12.0.0" } }, "sha512-UpOEIgWxWx0M+mDe1IMdHS6JuWM/L5nNIJ4ixX8v9JgBAejymo88OkgnmfLCNMem0Wd+b5vcQPWLdZybCndlsA=="],
"@vercel/static-config": ["@vercel/static-config@3.1.2", "", { "dependencies": { "ajv": "8.6.3", "json-schema-to-ts": "1.6.4", "ts-morph": "12.0.0" } }, "sha512-2d+TXr6K30w86a+WbMbGm2W91O0UzO5VeemZYBBUJbCjk/5FLLGIi8aV6RS2+WmaRvtcqNTn2pUA7nCOK3bGcQ=="],
"@vue/compiler-core": ["@vue/compiler-core@3.5.21", "", { "dependencies": { "@babel/parser": "^7.28.3", "@vue/shared": "3.5.21", "entities": "^4.5.0", "estree-walker": "^2.0.2", "source-map-js": "^1.2.1" } }, "sha512-8i+LZ0vf6ZgII5Z9XmUvrCyEzocvWT+TeR2VBUVlzIH6Tyv57E20mPZ1bCS+tbejgUgmjrEh7q/0F0bibskAmw=="],
@@ -2960,7 +2959,7 @@
"pino-std-serializers": ["pino-std-serializers@6.2.2", "", {}, "sha512-cHjPPsE+vhj/tnhCy/wiMh3M3z3h/j15zHQX+S9GkTBgqJuTuJzYJ4gUyACLhDaJ7kk9ba9iRDmbH2tJU03OiA=="],
"pip-requirements-js": ["pip-requirements-js@1.0.3", "", { "dependencies": { "ohm-js": "^17.1.0" } }, "sha512-1O9Bx0mPOZht3tW4LuxOA46qkD8A1AGymWXz3UwIMqGQgiTiOaFptsCf+9IE67qcbBrg8KHG6l8ePF7CoFRW/A=="],
"pip-requirements-js": ["pip-requirements-js@1.0.2", "", { "dependencies": { "ohm-js": "^17.1.0" } }, "sha512-awqoNOSOl4Blu4E4Hzp7jL0g8WKEhCwO+s7C2ibtIW3CAJMwspgoTXd4vnHd21UmhdrsI44Pn8FFSuA8QKrzvg=="],
"pirates": ["pirates@4.0.7", "", {}, "sha512-TfySrs/5nm8fQJDcBDuUng3VOUKsd7S+zqvbOTiGXHfxX4wK31ard+hoNuvkicM/2YFzlpDgABOevKSsB4G/FA=="],
@@ -3328,19 +3327,19 @@
"tsx": ["tsx@4.21.0", "", { "dependencies": { "esbuild": "~0.27.0", "get-tsconfig": "^4.7.5" }, "optionalDependencies": { "fsevents": "~2.3.3" }, "bin": { "tsx": "dist/cli.mjs" } }, "sha512-5C1sg4USs1lfG0GFb2RLXsdpXqBSEhAaA/0kPL01wxzpMqLILNxIxIOKiILz+cdg/pLnOUxFYOR5yhHU666wbw=="],
"turbo": ["turbo@2.8.16", "", { "optionalDependencies": { "turbo-darwin-64": "2.8.16", "turbo-darwin-arm64": "2.8.16", "turbo-linux-64": "2.8.16", "turbo-linux-arm64": "2.8.16", "turbo-windows-64": "2.8.16", "turbo-windows-arm64": "2.8.16" }, "bin": { "turbo": "bin/turbo" } }, "sha512-u6e9e3cTTpE2adQ1DYm3A3r8y3LAONEx1jYvJx6eIgSY4bMLxIxs0riWzI0Z/IK903ikiUzRPZ2c1Ph5lVLkhA=="],
"turbo": ["turbo@2.8.13", "", { "optionalDependencies": { "turbo-darwin-64": "2.8.13", "turbo-darwin-arm64": "2.8.13", "turbo-linux-64": "2.8.13", "turbo-linux-arm64": "2.8.13", "turbo-windows-64": "2.8.13", "turbo-windows-arm64": "2.8.13" }, "bin": { "turbo": "bin/turbo" } }, "sha512-nyM99hwFB9/DHaFyKEqatdayGjsMNYsQ/XBNO6MITc7roncZetKb97MpHxWf3uiU+LB9c9HUlU3Jp2Ixei2k1A=="],
"turbo-darwin-64": ["turbo-darwin-64@2.8.16", "", { "os": "darwin", "cpu": "x64" }, "sha512-KWa4hUMWrpADC6Q/wIHRkBLw6X6MV9nx6X7hSXbTrrMz0KdaKhmfudUZ3sS76bJFmgArBU25cSc0AUyyrswYxg=="],
"turbo-darwin-64": ["turbo-darwin-64@2.8.13", "", { "os": "darwin", "cpu": "x64" }, "sha512-PmOvodQNiOj77+Zwoqku70vwVjKzL34RTNxxoARjp5RU5FOj/CGiC6vcDQhNtFPUOWSAaogHF5qIka9TBhX4XA=="],
"turbo-darwin-arm64": ["turbo-darwin-arm64@2.8.16", "", { "os": "darwin", "cpu": "arm64" }, "sha512-NBgaqBDLQSZlJR4D5XCkQq6noaO0RvIgwm5eYFJYL3bH5dNu8o0UBpq7C5DYnQI8+ybyoHFjT5/icN4LeUYLow=="],
"turbo-darwin-arm64": ["turbo-darwin-arm64@2.8.13", "", { "os": "darwin", "cpu": "arm64" }, "sha512-kI+anKcLIM4L8h+NsM7mtAUpElkCOxv5LgiQVQR8BASyDFfc8Efj5kCk3cqxuxOvIqx0sLfCX7atrHQ2kwuNJQ=="],
"turbo-linux-64": ["turbo-linux-64@2.8.16", "", { "os": "linux", "cpu": "x64" }, "sha512-VYPdcCRevI9kR/hr1H1xwXy7QQt/jNKiim1e1mjANBXD2E9VZWMkIL74J1Huad5MbU3/jw7voHOqDPLJPC2p6w=="],
"turbo-linux-64": ["turbo-linux-64@2.8.13", "", { "os": "linux", "cpu": "x64" }, "sha512-j29KnQhHyzdzgCykBFeBqUPS4Wj7lWMnZ8CHqytlYDap4Jy70l4RNG46pOL9+lGu6DepK2s1rE86zQfo0IOdPw=="],
"turbo-linux-arm64": ["turbo-linux-arm64@2.8.16", "", { "os": "linux", "cpu": "arm64" }, "sha512-beq8tgUVI3uwkQkXJMiOr/hfxQRw54M3elpBwqgYFfemiK5LhCjjcwO0DkE8GZZfElBIlk+saMAQOZy3885wNQ=="],
"turbo-linux-arm64": ["turbo-linux-arm64@2.8.13", "", { "os": "linux", "cpu": "arm64" }, "sha512-OEl1YocXGZDRDh28doOUn49QwNe82kXljO1HXApjU0LapkDiGpfl3jkAlPKxEkGDSYWc8MH5Ll8S16Rf5tEBYg=="],
"turbo-windows-64": ["turbo-windows-64@2.8.16", "", { "os": "win32", "cpu": "x64" }, "sha512-Ig7b46iUgiOIkea/D3Z7H+zNzvzSnIJcLYFpZLA0RxbUTrbLhv9qIPwv3pT9p/abmu0LXVKHxaOo+p26SuDhzw=="],
"turbo-windows-64": ["turbo-windows-64@2.8.13", "", { "os": "win32", "cpu": "x64" }, "sha512-717bVk1+Pn2Jody7OmWludhEirEe0okoj1NpRbSm5kVZz/yNN/jfjbxWC6ilimXMz7xoMT3IDfQFJsFR3PMANA=="],
"turbo-windows-arm64": ["turbo-windows-arm64@2.8.16", "", { "os": "win32", "cpu": "arm64" }, "sha512-fOWjbEA2PiE2HEnFQrwNZKYEdjewyPc2no9GmrXklZnTCuMsxeCN39aVlKpKpim03Zq/ykIuvApGwq8ZbfS2Yw=="],
"turbo-windows-arm64": ["turbo-windows-arm64@2.8.13", "", { "os": "win32", "cpu": "arm64" }, "sha512-R819HShLIT0Wj6zWVnIsYvSNtRNj1q9VIyaUz0P24SMcLCbQZIm1sV09F4SDbg+KCCumqD2lcaR2UViQ8SnUJA=="],
"type": ["type@2.7.3", "", {}, "sha512-8j+1QmAbPvLZow5Qpi6NCaN8FB60p/6x8/vfNqOk/hC+HuvFZhL4+WfekuhQLiqFZXOgQdrs3B+XxEmCc6b3FQ=="],
@@ -3408,7 +3407,7 @@
"vary": ["vary@1.1.2", "", {}, "sha512-BNGbWLfd0eUPabhkXUVm0j8uuvREyTh5ovRa/dyow/BqAbZJyC+5fU+IzQOzmAKzYqYRAISoRhdQr3eIZ/PXqg=="],
"vercel": ["vercel@50.31.1", "", { "dependencies": { "@vercel/backends": "0.0.45", "@vercel/blob": "2.3.0", "@vercel/build-utils": "13.8.0", "@vercel/detect-agent": "1.2.0", "@vercel/elysia": "0.1.48", "@vercel/express": "0.1.57", "@vercel/fastify": "0.1.51", "@vercel/fun": "1.3.0", "@vercel/go": "3.4.4", "@vercel/h3": "0.1.57", "@vercel/hono": "0.2.51", "@vercel/hydrogen": "1.3.6", "@vercel/koa": "0.1.31", "@vercel/nestjs": "0.2.52", "@vercel/next": "4.16.1", "@vercel/node": "5.6.15", "@vercel/python": "6.22.0", "@vercel/redwood": "2.4.10", "@vercel/remix-builder": "5.7.0", "@vercel/ruby": "2.3.2", "@vercel/rust": "1.0.5", "@vercel/static-build": "2.9.0", "chokidar": "4.0.0", "esbuild": "0.27.0", "form-data": "^4.0.0", "jose": "5.9.6", "luxon": "^3.4.0", "proxy-agent": "6.4.0" }, "bin": { "vc": "dist/vc.js", "vercel": "dist/vc.js" } }, "sha512-cu8ukSJVuKecKavhMWMP/sgcetQ/6qsEaIGf9626tWXpRddUsLLJAsGyZUoZxo+EshMEp5kjuLZvTonyOUBLcA=="],
"vercel": ["vercel@50.26.1", "", { "dependencies": { "@vercel/backends": "0.0.40", "@vercel/blob": "2.3.0", "@vercel/build-utils": "13.6.2", "@vercel/detect-agent": "1.1.0", "@vercel/elysia": "0.1.43", "@vercel/express": "0.1.52", "@vercel/fastify": "0.1.46", "@vercel/fun": "1.3.0", "@vercel/go": "3.4.3", "@vercel/h3": "0.1.52", "@vercel/hono": "0.2.46", "@vercel/hydrogen": "1.3.5", "@vercel/koa": "0.1.26", "@vercel/nestjs": "0.2.47", "@vercel/next": "4.15.39", "@vercel/node": "5.6.10", "@vercel/python": "6.20.1", "@vercel/redwood": "2.4.9", "@vercel/remix-builder": "5.6.0", "@vercel/ruby": "2.3.2", "@vercel/rust": "1.0.5", "@vercel/static-build": "2.8.44", "chokidar": "4.0.0", "esbuild": "0.27.0", "form-data": "^4.0.0", "jose": "5.9.6", "luxon": "^3.4.0", "proxy-agent": "6.4.0" }, "bin": { "vc": "dist/vc.js", "vercel": "dist/vc.js" } }, "sha512-dFIsEkqTZYBNIZwe5cprBDe1a1AoWQXtBsk+/otAjuVk9WJ3x6kA9fE4rvEWAsnpk2jYj2ZbY04xjBt2cxGt5w=="],
"vfile": ["vfile@6.0.3", "", { "dependencies": { "@types/unist": "^3.0.0", "vfile-message": "^4.0.0" } }, "sha512-KzIbH/9tXat2u30jf+smMwFCsno4wHVdNmzFyL+T/L3UGqqk6JKfVqOFOZEpZSHADH1k40ab6NUIXZq422ov3Q=="],
+3 -3
View File
@@ -7,8 +7,8 @@
"devDependencies": {
"@biomejs/biome": "^1.9.4",
"@changesets/cli": "^2.30.0",
"turbo": "^2.8.16",
"vercel": "^50.31.1"
"turbo": "^2.8.13",
"vercel": "^50.26.1"
},
"packageManager": "bun@1.3.7",
"overrides": {
@@ -41,7 +41,7 @@
"catalog": {
"@tsconfig/strictest": "^2.0.6",
"@tsconfig/node20": "^20.1.6",
"@gitbook/api": "0.169.0",
"@gitbook/api": "0.167.0",
"@scalar/api-client-react": "^1.3.46",
"@types/react": "^19.0.0",
"@types/react-dom": "^19.0.0",
-6
View File
@@ -1,11 +1,5 @@
# @gitbook/embed
## 0.4.0
### Minor Changes
- b3875a1: Support `assistantName` property to override Assistant name
## 0.3.0
### Minor Changes
+1 -1
View File
@@ -13,7 +13,7 @@
}
},
"sideEffects": false,
"version": "0.4.0",
"version": "0.3.0",
"dependencies": {
"@gitbook/api": "catalog:",
"@gitbook/icons": "workspace:",
-19
View File
@@ -1,24 +1,5 @@
# gitbook
## 0.22.3
### Patch Changes
- f182680: Add backward compatibility for OpenAPI spec content URL
- bdc6cd9: Use OpenAPI publicURL as Scalar specUrl
- b3875a1: Refactor embeddable context to merge local & site properties in one unified way
- eceec6f: Move ~scalar/proxy route to site route group
- 887c3f8: Add token verification on OpenAPI proxy
- d666212: Add support for localized site section titles
- Updated dependencies [b3875a1]
- Updated dependencies [bdc6cd9]
- Updated dependencies [8ab419a]
- Updated dependencies [eceec6f]
- Updated dependencies [887c3f8]
- @gitbook/embed@0.4.0
- @gitbook/react-openapi@1.5.10
- @gitbook/openapi-parser@3.0.10
## 0.22.2
### Patch Changes
+2 -181
View File
@@ -16,7 +16,7 @@ import jwt from 'jsonwebtoken';
import { VISITOR_TOKEN_COOKIE } from '@/lib/visitors';
import { getGitBookPreviewURL, getSiteAPIToken } from '../tests/utils';
import { getSiteAPIToken } from '../tests/utils';
import {
type Test,
type TestsCase,
@@ -709,26 +709,6 @@ const testCases: TestsCase[] = [
await expect(page.locator('[data-testid="print-button"]')).toBeVisible();
},
},
{
name: 'Show error when missing token',
url: async () => {
const data = await getSiteAPIToken(
'https://gitbook.gitbook.io/test-gitbook-open/'
);
// Intentionally not setting the token to test error handling when the token is missing
const searchParams = new URLSearchParams();
searchParams.set('limit', '10');
return `~space/${data.space}/~gitbook/pdf?${searchParams.toString()}`;
},
screenshot: false,
run: async (page, response) => {
expect(response).not.toBeNull();
expect(response?.status()).toBe(400);
await expect(page.getByText('Missing API token')).toBeVisible();
},
},
],
},
{
@@ -774,157 +754,6 @@ const testCases: TestsCase[] = [
}
},
},
{
name: 'With customization cookie',
url: async () => {
const data = await getSiteAPIToken(
'https://gitbook.gitbook.io/test-gitbook-open/'
);
const searchParams = new URLSearchParams();
searchParams.set('token', data.apiToken);
return `url/preview/${data.site}/?${searchParams.toString()}`;
},
screenshot: false,
run: async (page) => {
await expect(page.locator('[data-testid="table-of-contents"]')).toBeVisible();
// Trademark exists by default
await expect(page.getByTestId('gb-trademark')).toHaveCount(1);
// Go to another page with the customization query to disable the trademark
const pageBlocks = new URL(page.url());
pageBlocks.pathname = `${pageBlocks.pathname.replace(/\/$/, '')}/blocks`;
pageBlocks.search = getCustomizationURL({
trademark: {
enabled: false,
},
}).slice(1);
await page.goto(pageBlocks.toString());
// No trademark because customization is disabled
await expect(page.getByTestId('gb-trademark')).toHaveCount(0);
await expect(
page.getByRole('heading', { level: 1, name: 'Blocks' })
).toBeVisible();
const pageBlocksCode = new URL(page.url());
pageBlocksCode.pathname = `${pageBlocksCode.pathname.replace(/\/$/, '')}/code`;
pageBlocksCode.search = '';
await page.goto(pageBlocksCode.toString());
// The trademark should not be visible because the cookie is still set,
await expect(page.getByTestId('gb-trademark')).toHaveCount(0);
await expect(
page.getByRole('heading', { level: 1, name: 'Code' })
).toBeVisible();
},
},
],
},
{
name: 'Site Previews',
skip: process.env.ARGOS_BUILD_NAME !== 'v2-vercel',
tests: [
{
name: 'Main content',
url: async () => {
const data = await getSiteAPIToken(
'https://gitbook.gitbook.io/test-gitbook-open/'
);
const searchParams = new URLSearchParams();
searchParams.set('token', data.apiToken);
return `url/${getGitBookPreviewURL(`${data.site}/?${searchParams.toString()}`)}`;
},
screenshot: false,
run: async (page) => {
await expect(page.locator('[data-testid="table-of-contents"]')).toBeVisible();
},
},
{
name: 'With sections',
url: async () => {
const data = await getSiteAPIToken('https://gitbook.com/docs');
const searchParams = new URLSearchParams();
searchParams.set('token', data.apiToken);
return `url/${getGitBookPreviewURL(`${data.site}/?${searchParams.toString()}`)}`;
},
screenshot: false,
run: async (page) => {
const sectionTabs = page.getByLabel('Sections');
await expect(sectionTabs).toBeVisible();
const sectionTabLinks = sectionTabs.getByRole('link');
for (const link of await sectionTabLinks.all()) {
const href = await link.getAttribute('href');
expect(href?.includes('/preview/site_p4Xo4')).toBeTruthy();
}
},
},
{
name: 'With customization cookie',
url: async () => {
const data = await getSiteAPIToken(
'https://gitbook.gitbook.io/test-gitbook-open/'
);
const searchParams = new URLSearchParams();
searchParams.set('token', data.apiToken);
return `url/${getGitBookPreviewURL(`${data.site}/?${searchParams.toString()}`)}`;
},
screenshot: false,
run: async (page) => {
await expect(page.locator('[data-testid="table-of-contents"]')).toBeVisible();
// Trademark exists by default
await expect(page.getByTestId('gb-trademark')).toHaveCount(1);
// Go to another page with the customization query to disable the trademark
const pageBlocks = new URL(page.url());
pageBlocks.pathname = `${pageBlocks.pathname.replace(/\/$/, '')}/blocks`;
pageBlocks.search = getCustomizationURL({
trademark: {
enabled: false,
},
}).slice(1);
await page.goto(pageBlocks.toString());
// No trademark because customization is disabled
await expect(page.getByTestId('gb-trademark')).toHaveCount(0);
await expect(
page.getByRole('heading', { level: 1, name: 'Blocks' })
).toBeVisible();
const pageBlocksCode = new URL(page.url());
pageBlocksCode.pathname = `${pageBlocksCode.pathname.replace(/\/$/, '')}/code`;
pageBlocksCode.search = '';
await page.goto(pageBlocksCode.toString());
// The trademark should not be visible because the cookie is still set,
await expect(page.getByTestId('gb-trademark')).toHaveCount(0);
await expect(
page.getByRole('heading', { level: 1, name: 'Code' })
).toBeVisible();
},
},
{
name: 'Redirect to app for authentication when missing token',
url: async () => {
const data = await getSiteAPIToken('https://gitbook.com/docs');
const searchParams = new URLSearchParams();
// Intentionally not setting the token to test redirection for authentication
return `url/${getGitBookPreviewURL(`${data.site}/?${searchParams.toString()}`)}`;
},
screenshot: false,
run: async (page) => {
await page.waitForURL(
(url) =>
url.host === 'app.gitbook.com' && url.pathname.includes('/preview/auth')
);
},
},
],
},
{
@@ -1508,21 +1337,13 @@ const testCases: TestsCase[] = [
contentBaseURL: 'https://gitbook-open-e2e-sites.gitbook.io/gitbook-doc/',
tests: [
{
name: 'Basic redirect',
name: 'Redirect to SSO page',
url: 'a/redirect/to/sso',
run: async (page) => {
await expect(page.locator('h1')).toHaveText('SSO');
},
screenshot: false,
},
{
name: 'Complex wildcard with special characters',
url: 'foo/bar/baz/123456789-welcome-to-gitbook-%22%20target=%22_blank',
run: async (page) => {
await expect(page.locator('h1')).toHaveText('SEO');
},
screenshot: false,
},
],
},
{
-1
View File
@@ -27,7 +27,6 @@ const nextConfig = {
GITBOOK_API_URL: process.env.GITBOOK_API_URL,
GITBOOK_APP_URL: process.env.GITBOOK_APP_URL,
GITBOOK_OAUTH_SERVER_URL: process.env.GITBOOK_OAUTH_SERVER_URL,
GITBOOK_PREVIEW_BASE_URL: process.env.GITBOOK_PREVIEW_BASE_URL,
GITBOOK_INTEGRATIONS_HOST: process.env.GITBOOK_INTEGRATIONS_HOST,
GITBOOK_IMAGE_RESIZE_URL: process.env.GITBOOK_IMAGE_RESIZE_URL,
GITBOOK_ICONS_URL: process.env.GITBOOK_ICONS_URL,
@@ -8,9 +8,6 @@
"allow_importable_env",
"global_fetch_strictly_public"
],
"observability": {
"enabled": false
},
"vars": {
"NEXT_CACHE_DO_QUEUE_DISABLE_SQLITE": "true"
},
@@ -8,9 +8,6 @@
"allow_importable_env",
"global_fetch_strictly_public"
],
"observability": {
"enabled": false
},
"env": {
"preview": {
"vars": {
@@ -8,9 +8,6 @@
"allow_importable_env",
"global_fetch_strictly_public"
],
"observability": {
"enabled": false
},
"assets": {
"directory": "../../.open-next/assets",
"binding": "ASSETS"
+1 -2
View File
@@ -1,6 +1,6 @@
{
"name": "gitbook",
"version": "0.22.3",
"version": "0.22.2",
"private": true,
"dependencies": {
"@cloudflare/workers-types": "^4.20251011.0",
@@ -63,7 +63,6 @@
"react": "catalog:",
"react-dom": "catalog:",
"react-hotkeys-hook": "^4.4.1",
"rehype-raw": "^7.0.0",
"rehype-sanitize": "^6.0.0",
"rehype-stringify": "^10.0.1",
"remark-gfm": "^4.0.1",
@@ -9,7 +9,7 @@ import { throwIfDataError } from '@/lib/data';
import { getSiteURLDataFromMiddleware } from '@/lib/middleware';
import { joinPathWithBaseURL } from '@/lib/paths';
import { getServerActionBaseContext } from '@/lib/server-actions';
import { findSiteSpaceBy, getLocalizedTitle } from '@/lib/sites';
import { findSiteSpaceBy } from '@/lib/sites';
import type {
SearchPageResult,
SearchSpaceResult,
@@ -108,7 +108,6 @@ function transformSitePageResult(
): OrderedComputedResult[] {
const { pageItem, spaceItem, spaceURL, siteSection, siteSectionGroup, siteSpace } = args;
const { linker } = context;
const currentLanguage = siteSpace?.space.language;
const page: ComputedPageResult = {
type: 'page',
@@ -122,11 +121,11 @@ function transformSitePageResult(
breadcrumbs: [
siteSectionGroup && {
icon: siteSectionGroup?.icon as IconName,
label: getLocalizedTitle(siteSectionGroup, currentLanguage),
label: siteSectionGroup.title,
},
siteSection && {
icon: siteSection?.icon as IconName,
label: getLocalizedTitle(siteSection, currentLanguage),
label: siteSection.title,
},
(siteSection?.siteSpaces?.filter(
(space) =>
@@ -135,7 +134,7 @@ function transformSitePageResult(
).length > 1
).length ?? 0) > 1 && siteSpace
? {
label: getLocalizedTitle(siteSpace, currentLanguage),
label: siteSpace.title,
}
: undefined,
...pageItem.ancestors.map((ancestor) => ({
@@ -41,14 +41,14 @@ export const CodeBlockRenderer = forwardRef(function CodeBlockRenderer(
ref={ref}
aria-busy={ariaBusy}
className={tcls(
'group/codeblock shiki relative flex shrink flex-col overflow-hidden print:overflow-visible',
'group/codeblock shiki grid shrink grid-flow-col overflow-hidden',
style
)}
/* Sets the code theme's mode (light or dark) for the site's theme mode (light or dark).
* Used to style UI elements (scrollbars, form controls) correctly and apply the right default to "plain" code blocks. */
data-color-scheme={`${theme.themes.light.type} ${theme.themes.dark.type}`}
>
<div className="flex items-center justify-start gap-2 text-sm">
<div className="flex items-center justify-start gap-2 text-sm [grid-area:1/1]">
{title ? (
<div
className="relative top-px z-20 inline-flex items-center justify-center circular-corners:rounded-t-xl rounded-corners:rounded-t-lg straight-corners:rounded-t-xs border border-tint-subtle border-b-0 bg-tint-subtle theme-bold-tint:bg-tint-base theme-muted:bg-tint-base px-3 py-2 text-tint text-xs leading-none tracking-wide contrast-more:border-tint contrast-more:bg-tint-base [html.theme-bold.sidebar-filled_&]:bg-tint-base"
@@ -65,11 +65,11 @@ export const CodeBlockRenderer = forwardRef(function CodeBlockRenderer(
</div>
<CopyCodeButton
codeId={codeId}
style="absolute top-2 right-2 z-2 self-start justify-self-end leading-none opacity-0 backdrop-blur-md group-hover/codeblock:opacity-11"
style="z-2 mt-2 mr-2 self-start justify-self-end leading-none opacity-0 backdrop-blur-md [grid-area:2/1] group-hover/codeblock:opacity-11"
/>
<pre
className={tcls(
'relative overflow-auto border border-tint-subtle bg-tint-subtle theme-bold-tint:bg-tint-base theme-muted:bg-tint-base p-2 text-tint-strong contrast-more:border-tint contrast-more:bg-tint-base print:overflow-visible',
'relative overflow-auto border border-tint-subtle bg-tint-subtle theme-bold-tint:bg-tint-base theme-muted:bg-tint-base p-2 text-tint-strong [grid-area:2/1] contrast-more:border-tint contrast-more:bg-tint-base',
'circular-corners:rounded-2xl rounded-corners:rounded-xl straight-corners:rounded-xs depth-subtle:shadow-xs',
title && 'rounded-ss-none!'
)}
@@ -83,7 +83,7 @@ export const CodeBlockRenderer = forwardRef(function CodeBlockRenderer(
<code
id={codeId}
className={tcls(
'table max-h-full w-fit [counter-reset:line] print:max-h-none print:whitespace-pre-wrap',
'inline-grid max-h-full min-w-full grid-cols-[auto_1fr] [count-reset:line] print:whitespace-pre-wrap',
withWrap && 'whitespace-pre-wrap',
'[[aria-expanded=false]_&]:mask-b-from-50%'
)}
@@ -26,7 +26,7 @@ async function OpenAPIOperationBody(props: BlockProps<AnyOpenAPIOperationsBlock>
return null;
}
const { data, specUrl, publicURL, error } = await resolveOpenAPIOperationBlock({
const { data, specUrl, error } = await resolveOpenAPIOperationBlock({
block,
context: context.contentContext,
});
@@ -46,11 +46,7 @@ async function OpenAPIOperationBody(props: BlockProps<AnyOpenAPIOperationsBlock>
return (
<BaseOpenAPIOperation
data={data}
context={getOpenAPIContext({
props,
specUrl: publicURL,
context: context.contentContext,
})}
context={getOpenAPIContext({ props, specUrl, context: context.contentContext })}
className="openapi-block"
/>
);
@@ -25,7 +25,7 @@ async function OpenAPISchemasBody(props: BlockProps<OpenAPISchemasBlock>) {
return null;
}
const { data, specUrl, publicURL, error } = await resolveOpenAPISchemasBlock({
const { data, specUrl, error } = await resolveOpenAPISchemasBlock({
block,
context: context.contentContext,
});
@@ -48,11 +48,7 @@ async function OpenAPISchemasBody(props: BlockProps<OpenAPISchemasBlock>) {
<BaseOpenAPISchemas
schemas={data.schemas}
grouped={block.data.grouped}
context={getOpenAPIContext({
props,
specUrl: publicURL,
context: context.contentContext,
})}
context={getOpenAPIContext({ props, specUrl, context: context.contentContext })}
className="openapi-block"
/>
);
@@ -26,7 +26,7 @@ async function OpenAPIWebhookBody(props: BlockProps<OpenAPIWebhookBlock>) {
return null;
}
const { data, specUrl, publicURL, error } = await resolveOpenAPIWebhookBlock({
const { data, specUrl, error } = await resolveOpenAPIWebhookBlock({
block,
context: context.contentContext,
});
@@ -48,11 +48,7 @@ async function OpenAPIWebhookBody(props: BlockProps<OpenAPIWebhookBlock>) {
return (
<BaseOpenAPIWebhook
data={data}
context={getOpenAPIContext({
props,
specUrl: publicURL,
context: context.contentContext,
})}
context={getOpenAPIContext({ props, specUrl, context: context.contentContext })}
className="openapi-block"
/>
);
@@ -24,7 +24,7 @@ import type {
*/
export function getOpenAPIContext(args: {
props: BlockProps<AnyOpenAPIOperationsBlock | OpenAPISchemasBlock | OpenAPIWebhookBlock>;
specUrl: string | null;
specUrl: string;
context: GitBookAnyContext | undefined;
}): OpenAPIContextInput {
const { props, specUrl, context } = args;
@@ -3,10 +3,8 @@ import type { GitBookSiteContext } from '@/lib/context';
import { SiteInsightsDisplayContext } from '@gitbook/api';
import type { Metadata } from 'next';
import { HeaderMobileMenu } from '../Header/HeaderMobileMenu';
import { SpacesDropdown, TranslationsDropdown } from '../Header/SpacesDropdown';
import { PageBody } from '../PageBody';
import { SiteSectionTabs, encodeClientSiteSections } from '../SiteSections';
import { categorizeVariants } from '../SpaceLayout/categorizeVariants';
import { TableOfContents } from '../TableOfContents';
import { ScrollContainer } from '../primitives/ScrollContainer';
import { EmbeddableDocsPageControlButtons } from './EmbeddableDocsPageControlButtons';
@@ -45,8 +43,6 @@ export async function EmbeddableDocsPage(
pageParams,
});
const variants = categorizeVariants(context);
return (
<EmbeddableFrame className="site-background">
<EmbeddableFrameSidebar>
@@ -80,20 +76,7 @@ export async function EmbeddableDocsPage(
<SiteSectionTabs
className="not-theme-bold:-mt-2 theme-bold:bg-tint-base"
sections={encodeClientSiteSections(context, context.sections)}
>
{variants.translations.length > 1 ? (
<TranslationsDropdown
context={context}
siteSpace={
variants.translations.find(
(space) => space.id === context.siteSpace.id
) ?? context.siteSpace
}
siteSpaces={variants.translations}
className="my-1.5 ml-2 self-start"
/>
) : null}
</SiteSectionTabs>
/>
) : null}
</div>
<EmbeddableFrameBody>
@@ -104,38 +87,7 @@ export async function EmbeddableDocsPage(
leading={{ fade: false, button: true }}
trailing={{ fade: false, button: true }}
>
<TableOfContents
context={context}
withTrademark={false}
header={
!context.sections && variants.translations.length > 1 ? (
<TranslationsDropdown
context={context}
siteSpace={
variants.translations.find(
(space) => space.id === context.siteSpace.id
) ?? context.siteSpace
}
siteSpaces={variants.translations}
className="max-md:[&_.button-content]:block"
/>
) : null
}
innerHeader={
variants.generic.length > 1 ? (
<div className="my-5 sidebar-default:mt-2 flex flex-col gap-2 px-5 empty:hidden">
{variants.generic.length > 1 ? (
<SpacesDropdown
context={context}
siteSpace={context.siteSpace}
siteSpaces={variants.generic}
className="w-full px-3"
/>
) : null}
</div>
) : null
}
/>
<TableOfContents context={context} withTrademark={false} />
<PageBody
context={context}
page={page}
@@ -2,7 +2,7 @@ import type { SiteSpace } from '@gitbook/api';
import { useMemo } from 'react';
import type { GitBookSiteContext } from '@/lib/context';
import { getLocalizedTitle, getSiteSpaceURL } from '@/lib/sites';
import { getSiteSpaceURL } from '@/lib/sites';
import { tcls } from '@/lib/tailwind';
import { Button, type ButtonProps, ToggleChevron } from '../primitives';
import { DropdownMenu } from '../primitives/DropdownMenu';
@@ -24,7 +24,6 @@ export function SpacesDropdown(props: {
icon?: ButtonProps['icon'];
}) {
const { context, siteSpace, siteSpaces, className, variant = 'secondary', icon } = props;
const currentLanguage = context.siteSpace.space.language;
return (
<DropdownMenu
@@ -41,16 +40,14 @@ export function SpacesDropdown(props: {
trailing={<ToggleChevron />}
className={tcls('bg-tint-base', className)}
>
<span className="button-content">
{getLocalizedTitle(siteSpace, currentLanguage)}
</span>
<span className="button-content">{siteSpace.title}</span>
</Button>
}
>
<SpacesDropdownMenuItems
slimSpaces={siteSpaces.map((siteSp) => ({
id: siteSp.id,
title: getLocalizedTitle(siteSp, currentLanguage),
title: siteSp.title,
url: getSiteSpaceURL(context, siteSp),
isActive: siteSp.id === siteSpace.id,
spaceId: siteSp.space.id,
@@ -70,8 +67,7 @@ export function TranslationsDropdown(props: {
const { context, siteSpace, siteSpaces, className } = props;
// Memoize the emoji check to avoid repeated regex execution
const title = getLocalizedTitle(siteSpace, context.siteSpace.space.language);
const hasEmojiPrefix = useMemo(() => startsWithEmoji(title), [title]);
const hasEmojiPrefix = useMemo(() => startsWithEmoji(siteSpace.title), [siteSpace.title]);
return (
<SpacesDropdown
@@ -319,7 +319,7 @@ html.dark .shiki span {
}
.highlight-line {
@apply min-w-min text-tint-strong table-row bg-tint-subtle theme-muted:bg-tint-base theme-bold-tint:bg-tint-base relative hover:invert-5 hover:z-1 rounded-sm;
@apply text-tint-strong grid grid-cols-subgrid col-span-2 bg-tint-subtle theme-muted:bg-tint-base theme-bold-tint:bg-tint-base relative hover:invert-5 hover:z-1 rounded-sm;
@apply only:hover:ring-transparent;
@apply [counter-increment:line];
@@ -345,7 +345,7 @@ html.dark .shiki span {
}
.highlight-line-number {
@apply table-cell whitespace-nowrap text-sm text-tint pr-4 pl-2 text-right bg-tint-subtle theme-muted:bg-tint-base theme-bold-tint:bg-tint-base rounded-l sticky -left-2;
@apply text-sm text-tint text-right pr-3.5 bg-tint-subtle theme-muted:bg-tint-base theme-bold-tint:bg-tint-base rounded-l pl-2 sticky left-[-3px] mask-r-from-80%;
@apply before:content-[counter(line)] not-contrast-more:before:opacity-6;
.highlighted & {
@@ -354,5 +354,5 @@ html.dark .shiki span {
}
.highlight-line-content {
@apply table-cell text-sm;
@apply ml-3 block text-sm;
}
@@ -1,7 +1,6 @@
'use client';
import { t, useLanguage } from '@/intl/client';
import { getLocalizedTitle } from '@/lib/sites';
import { CustomizationSearchStyle, type SiteSection, type SiteSpace } from '@gitbook/api';
import { useRouter } from 'next/navigation';
import React, { useRef } from 'react';
@@ -257,10 +256,7 @@ export function SearchContainer({
<div className="border-tint-subtle border-t bg-tint-subtle px-4 py-1.5">
<SearchScopeControl
section={section}
spaceTitle={getLocalizedTitle(
siteSpace,
siteSpace.space.language
)}
spaceTitle={siteSpace.title}
withVariants={withVariants}
withSiteVariants={withSiteVariants}
withSections={withSections}
@@ -17,7 +17,7 @@ import { isPageIndexable, isSiteIndexable } from '@/lib/seo';
import { getResizedImageURL } from '@/lib/images';
import { resolveContentRef } from '@/lib/references';
import { getLocalizedTitle } from '@/lib/sites';
import { getSiteSectionTitle } from '@/lib/sites';
import { tcls } from '@/lib/tailwind';
import { getPageRSSURL } from '@/routes/rss';
import { PageContextProvider } from '../PageContext';
@@ -134,14 +134,14 @@ function getSiteStructureTitle(context: GitBookSiteContext): string | null {
sections.current.default === false && // Only if the current section is not the default one
sections.list.filter((section) => section.object === 'site-section').length > 1 // Only if there are multiple sections
) {
title.push(getLocalizedTitle(sections.current, currentLanguage));
title.push(getSiteSectionTitle(sections.current, currentLanguage));
}
if (
siteSpaces.length > 1 && // Only if there are multiple variants
siteSpace.default === false && // Only if the variant is not the default one
siteSpaces.filter((space) => space.space.language === siteSpace.space.language).length > 1 // Only if there are multiple variants *for the current language*. This filters out spaces that are "just" translations of each other, not versions.
) {
title.push(getLocalizedTitle(siteSpace, siteSpace.space.language));
title.push(siteSpace.title);
}
return title.join(' ');
}
@@ -1,11 +1,6 @@
import type { GitBookSiteContext } from '@/lib/context';
import { redirect } from 'next/navigation';
import {
SITE_REDIRECT_SOURCE_PATH_MAX_LENGTH,
SITE_REDIRECT_SOURCE_PATH_PATTERN,
} from '@gitbook/api';
import { getDataOrNull } from '@/lib/data';
import { resolvePageId, resolvePagePath } from '@/lib/pages';
import { withLeadingSlash } from '@/lib/paths';
@@ -58,37 +53,38 @@ async function resolvePage(context: GitBookSiteContext, params: PagePathParams |
// We don't test path that are too long as GitBook doesn't support them and will return a 404 anyway.
// API has a limit of less than 512 characters for the source path, so we use the same limit here.
if (rawPathname.length < SITE_REDIRECT_SOURCE_PATH_MAX_LENGTH) {
const SITE_REDIRECT_SOURCE_PATH_REGEX = new RegExp(SITE_REDIRECT_SOURCE_PATH_PATTERN);
if (rawPathname.length < 512) {
// Duplicated the regex pattern from SiteRedirectSourcePath API type.
const SITE_REDIRECT_SOURCE_PATH_REGEX =
/^\/(?:[A-Za-z0-9\-._~]|%[0-9A-Fa-f]{2})+(?:\/(?:[A-Za-z0-9\-._~]|%[0-9A-Fa-f]{2})+)*$/;
const redirectPathname = withLeadingSlash(rawPathname);
// If a page can't be found, we try with the API, in case we have a redirect at site level.
const redirectSources = new Set(
[
if (SITE_REDIRECT_SOURCE_PATH_REGEX.test(redirectPathname)) {
const redirectSources = new Set<string>([
// Test the pathname relative to the root
// For example hello/world -> section/variant/hello/world
linker.toRelativePathInSite(linker.toPathInSpace(redirectPathname)),
withLeadingSlash(
linker.toRelativePathInSite(linker.toPathInSpace(redirectPathname))
),
// Test the pathname relative to the content/space
// For example hello/world -> /hello/world
redirectPathname,
]
.map(toSiteRedirectSourceCandidate)
.filter((source) => SITE_REDIRECT_SOURCE_PATH_REGEX.test(source))
);
for (const source of redirectSources) {
// We try to resolve the site redirect
const resolvedSiteRedirect =
source.length < SITE_REDIRECT_SOURCE_PATH_MAX_LENGTH &&
(await getDataOrNull(
context.dataFetcher.getSiteRedirectBySource({
organizationId,
siteId: site.id,
source,
siteShareKey: shareKey,
})
));
if (resolvedSiteRedirect) {
return redirect(linker.toLinkForContent(resolvedSiteRedirect.target));
]);
for (const source of redirectSources) {
// We try to resolve the site redirect
const resolvedSiteRedirect =
source.length < 512 &&
(await getDataOrNull(
context.dataFetcher.getSiteRedirectBySource({
organizationId,
siteId: site.id,
source,
siteShareKey: shareKey,
})
));
if (resolvedSiteRedirect) {
return redirect(linker.toLinkForContent(resolvedSiteRedirect.target));
}
}
}
@@ -110,21 +106,6 @@ async function resolvePage(context: GitBookSiteContext, params: PagePathParams |
return undefined;
}
/**
* Transform a pathname into a candidate source for site redirect matching.
* We also encode each segment to handle special characters in redirects.
*/
function toSiteRedirectSourceCandidate(pathname: string): string {
const normalized = withLeadingSlash(pathname);
return withLeadingSlash(
normalized
.slice(1)
.split('/')
.map((segment) => encodeURIComponent(segment))
.join('/')
);
}
/**
* Get the page path from the params.
*/
@@ -1,10 +1,5 @@
import type { GitBookSiteContext, SiteSections } from '@/lib/context';
import {
getLocalizedDescription,
getLocalizedTitle,
getSectionURL,
getSiteSpaceURL,
} from '@/lib/sites';
import { getSectionURL, getSiteSectionTitle, getSiteSpaceURL } from '@/lib/sites';
import type { SiteSection, SiteSectionGroup, SiteSpace } from '@gitbook/api';
import assertNever from 'assert-never';
@@ -29,7 +24,6 @@ export type ClientSiteSectionGroup = Pick<SiteSectionGroup, 'id' | 'title' | 'ic
*/
export function encodeClientSiteSections(context: GitBookSiteContext, sections: SiteSections) {
const { list, current } = sections;
const currentLanguage = context.siteSpace.space.language;
const clientSections: (ClientSiteSection | ClientSiteSectionGroup)[] = [];
@@ -45,7 +39,7 @@ export function encodeClientSiteSections(context: GitBookSiteContext, sections:
clientSections.push({
id: item.id,
title: getLocalizedTitle(item, currentLanguage),
title: item.title,
icon: item.icon,
object: item.object,
children,
@@ -72,7 +66,6 @@ function encodeChildren(
children: (SiteSection | SiteSectionGroup)[]
): (ClientSiteSection | ClientSiteSectionGroup)[] {
const clientChildren: (ClientSiteSection | ClientSiteSectionGroup)[] = [];
const currentLanguage = context.siteSpace.space.language;
for (const child of children) {
switch (child.object) {
@@ -90,7 +83,7 @@ function encodeChildren(
clientChildren.push({
id: child.id,
title: getLocalizedTitle(child, currentLanguage),
title: child.title,
icon: child.icon,
object: child.object,
children: nestedChildren,
@@ -109,8 +102,8 @@ function encodeSection(context: GitBookSiteContext, section: SiteSection) {
const currentLanguage = context.siteSpace.space.language;
return {
id: section.id,
title: getLocalizedTitle(section, currentLanguage),
description: getLocalizedDescription(section, currentLanguage),
title: getSiteSectionTitle(section, currentLanguage),
description: section.description,
icon: section.icon,
object: section.object,
url: findBestTargetURL(context, section),
@@ -25,7 +25,6 @@ export function Trademark(props: {
return (
<Button
data-testid="gb-trademark"
target="_blank"
variant="secondary"
size="large"
@@ -1,17 +1,17 @@
// Bun Snapshot v1, https://bun.sh/docs/test/snapshots
// Bun Snapshot v1, https://goo.gl/fbAQLP
exports[`parseMarkdown should parse a simple table 1`] = `
"<h2>Table</h2>
<table><thead><tr><th>a</th><th align="left">b</th><th align="right">c</th><th align="center">d</th></tr></thead></table>"
<table>
<thead>
<tr>
<th>a</th>
<th align="left">b</th>
<th align="right">c</th>
<th align="center">d</th>
</tr>
</thead>
</table>"
`;
exports[`parseMarkdown should parse a complex table 1`] = `
@@ -31,176 +31,116 @@ A serialnumbers value governs its order within the given token.id</p>
<h2>Filtering</h2>
<p>When filtering there are some restrictions enforced to ensure correctness and scalability.</p>
<p><strong>The table below defines the restrictions and support for the NFT ownership endpoint</strong></p>
<table><thead><tr><th>Query Param</th><th>Comparison Operator</th><th>Support</th><th>Description</th><th>Example</th></tr></thead><tbody><tr><td>token.id</td><td>eq</td><td>Y</td><td>Single occurrence only.</td><td>?token.id=X</td></tr><tr><td></td><td>ne</td><td>N</td><td></td><td></td></tr><tr><td></td><td>lt(e)</td><td>Y</td><td>Single occurrence only.</td><td>?token.id=lte:X</td></tr><tr><td></td><td>gt(e)</td><td>Y</td><td>Single occurrence only.</td><td>?token.id=gte:X</td></tr><tr><td>serialnumber</td><td>eq</td><td>Y</td><td>Single occurrence only. Requires the presence of a <strong>token.id</strong> query</td><td>?serialnumber=Y</td></tr><tr><td></td><td>ne</td><td>N</td><td></td><td></td></tr><tr><td></td><td>lt(e)</td><td>Y</td><td>Single occurrence only. Requires the presence of an <strong>lte</strong> or <strong>eq</strong> <strong>token.id</strong> query</td><td>?token.id=lte:X&#x26;serialnumber=lt:Y</td></tr><tr><td></td><td>gt(e)</td><td>Y</td><td>Single occurrence only. Requires the presence of an <strong>gte</strong> or <strong>eq</strong> <strong>token.id</strong> query</td><td>?token.id=gte:X&#x26;serialnumber=gt:Y</td></tr><tr><td>spender.id</td><td>eq</td><td>Y</td><td></td><td>?spender.id=Z</td></tr><tr><td></td><td>ne</td><td>N</td><td></td><td></td></tr><tr><td></td><td>lt(e)</td><td>Y</td><td></td><td>?spender.id=lt:Z</td></tr><tr><td></td><td>gt(e)</td><td>Y</td><td></td><td>?spender.id=gt:Z</td></tr></tbody></table>
<table>
<thead>
<tr>
<th>Query Param</th>
<th>Comparison Operator</th>
<th>Support</th>
<th>Description</th>
<th>Example</th>
</tr>
</thead>
<tbody>
<tr>
<td>token.id</td>
<td>eq</td>
<td>Y</td>
<td>Single occurrence only.</td>
<td>?token.id=X</td>
</tr>
<tr>
<td></td>
<td>ne</td>
<td>N</td>
<td></td>
<td></td>
</tr>
<tr>
<td></td>
<td>lt(e)</td>
<td>Y</td>
<td>Single occurrence only.</td>
<td>?token.id=lte:X</td>
</tr>
<tr>
<td></td>
<td>gt(e)</td>
<td>Y</td>
<td>Single occurrence only.</td>
<td>?token.id=gte:X</td>
</tr>
<tr>
<td>serialnumber</td>
<td>eq</td>
<td>Y</td>
<td>Single occurrence only. Requires the presence of a <strong>token.id</strong> query</td>
<td>?serialnumber=Y</td>
</tr>
<tr>
<td></td>
<td>ne</td>
<td>N</td>
<td></td>
<td></td>
</tr>
<tr>
<td></td>
<td>lt(e)</td>
<td>Y</td>
<td>Single occurrence only. Requires the presence of an <strong>lte</strong> or <strong>eq</strong> <strong>token.id</strong> query</td>
<td>?token.id=lte:X&#x26;serialnumber=lt:Y</td>
</tr>
<tr>
<td></td>
<td>gt(e)</td>
<td>Y</td>
<td>Single occurrence only. Requires the presence of an <strong>gte</strong> or <strong>eq</strong> <strong>token.id</strong> query</td>
<td>?token.id=gte:X&#x26;serialnumber=gt:Y</td>
</tr>
<tr>
<td>spender.id</td>
<td>eq</td>
<td>Y</td>
<td></td>
<td>?spender.id=Z</td>
</tr>
<tr>
<td></td>
<td>ne</td>
<td>N</td>
<td></td>
<td></td>
</tr>
<tr>
<td></td>
<td>lt(e)</td>
<td>Y</td>
<td></td>
<td>?spender.id=lt:Z</td>
</tr>
<tr>
<td></td>
<td>gt(e)</td>
<td>Y</td>
<td></td>
<td>?spender.id=gt:Z</td>
</tr>
</tbody>
</table>
<p>Note: When searching across a range for individual NFTs a <strong>serialnumber</strong> with an additional <strong>token.id</strong> query filter must be provided.
Both filters must be a single occurrence of <strong>gt(e)</strong> or <strong>lt(e)</strong> which provide a lower and or upper boundary for search.</p>"
`;
exports[`parseMarkdown should parse html 1`] = `
"<div>
<div>
<div>
ID:
</div>
<div>
get_trending_events
</div>
</div>
<div>
<div>
<span>
<span>
</span>
<span>
sports_events_trending
</span>
</span>
</div>
</div>
</div>
<div>
<span>
Sports
</span>
<span>
Personalization
</span>
</div>
<p>Returns trending events for the given time frame.</p>
"<p>Returns trending events for the given time frame.</p>
<p>The events are sorted by <code>trending score</code>.</p>
<div>
<div>
<span>
</span>
<span>
Trending score
</span>
</div>
<div>
<p>To calculate trending score for an event, each bet placed on this event
contributes a value determined by how recently the bet was placed.</p>
<p>Recent bets add more value than older ones.</p>
<p>The normalized value of its score is also assigned to each event as its confidence.</p>
</div>
</div>
<div>
<div>
<span>
</span>
<span>
Filtering example
</span>
</div>
<div>
<p>In this example we get all trending events where <code>league</code> is <code>UEFA Champions League</code>.</p>
<pre><code class="language-bash">$ curl --request GET \\
--url 'https://api.vaix.ai/api/sports/events/trending?filters=league%3Aeq%3AUEFA%20Champions%20League'
</code></pre>
</div>
</div>"
</code></pre>"
`;
-1
View File
@@ -45,7 +45,6 @@ export type SiteURLData = Pick<
| 'siteBasePath'
| 'basePath'
| 'contextId'
| 'preview'
> & {
/**
* Identifier used for image resizing.
+1 -8
View File
@@ -78,7 +78,6 @@ export async function ignoreAllThrownError<T>(promise: Promise<T>): Promise<T |
/**
* Wrap an async execution to handle errors and return a DataFetcherResponse.
* This function should not throw.
*/
export async function wrapDataFetcherError<T>(
fn: () => Promise<T>
@@ -179,7 +178,6 @@ export function extractCacheControl(error: GitBookAPIError) {
/**
* Get a data fetcher exposable error from a JS error.
* This function should never throw, even if the error is not in the expected format. In that case, it should return a generic error with code 500.
*/
export function getExposableError(error: Error): DataFetcherErrorData {
if (error instanceof GitBookAPIError) {
@@ -199,10 +197,5 @@ export function getExposableError(error: Error): DataFetcherErrorData {
};
}
console.warn('An unexpected error occurred', error);
return {
code: 500,
message: 'An unexpected error occurred',
};
throw error;
}
-6
View File
@@ -64,12 +64,6 @@ export const GITBOOK_USER_AGENT = process.env.GITBOOK_USER_AGENT || 'GitBook-Ope
export const GITBOOK_OAUTH_SERVER_URL =
process.env.GITBOOK_OAUTH_SERVER_URL || 'https://sites.gitbook.com/oauth2/v1';
/**
* Base URL for GitBook preview requests.
*/
export const GITBOOK_PREVIEW_BASE_URL =
process.env.GITBOOK_PREVIEW_BASE_URL || 'https://sites.gitbook.com/preview/';
/**
* Whether to disable tracking of events into site insights.
* This is used to disable tracking in development mode.
+7 -4
View File
@@ -1,4 +1,3 @@
import rehypeRaw from 'rehype-raw';
import rehypeSanitize from 'rehype-sanitize';
import rehypeStringify from 'rehype-stringify';
import remarkGfm from 'remark-gfm';
@@ -6,14 +5,18 @@ import remarkParse from 'remark-parse';
import remarkRehype from 'remark-rehype';
import { unified } from 'unified';
/**
* Parse markdown and output HTML.
*/
export async function parseMarkdown(markdown: string): Promise<string> {
return unified()
const promise = unified()
.use(remarkParse)
.use(remarkGfm)
.use(remarkRehype, { allowDangerousHtml: true })
.use(rehypeRaw)
.use(remarkRehype)
.use(rehypeSanitize)
.use(rehypeStringify)
.process(markdown)
.then((file) => file.toString());
return promise;
}
@@ -13,7 +13,7 @@ describe('#enrichFilesystem', () => {
});
const enriched = await enrichFilesystem(filesystem);
expect(enriched[0]?.specification.paths['/pet'].put['x-gitbook-description-html']).toBe(
'<p>Social platform<br>Hello</p>'
'<p>Social platform</p>'
);
});
});
+4 -18
View File
@@ -33,14 +33,14 @@ export async function fetchOpenAPIFilesystem(
const resolved = ref ? await resolveContentRef(ref, context) : null;
if (!resolved) {
return { filesystem: null, specUrl: null, publicURL: null };
return { filesystem: null, specUrl: null };
}
const result = await (() => {
// If the reference is a new OpenAPI reference, we return it.
if (ref.kind === 'openapi') {
assert(resolved.openapi?.filesystem);
return resolved.openapi.filesystem;
assert(resolved.openAPIFilesystem);
return resolved.openAPIFilesystem;
}
// For legacy blocks ("swagger"), we need to fetch the file system.
return fetchFilesystem(resolved.href, context.space.id);
@@ -50,21 +50,7 @@ export async function fetchOpenAPIFilesystem(
throw new OpenAPIParseError(result.error.message, { code: result.error.code });
}
const publicURL = (() => {
// For new OpenAPI refs, use the explicit publicURL (null when spec is private).
if (ref.kind === 'openapi' && resolved.openapi) {
return resolved.openapi.publicURL;
}
// For legacy "swagger" refs, the href is the public spec URL itself.
return resolved.href;
})();
return {
filesystem: result,
specUrl: resolved.href,
publicURL,
};
return { filesystem: result, specUrl: resolved.href };
}
/**
@@ -9,7 +9,7 @@ paths:
put:
summary: Update an existing pet
description: |
Social platform<br>Hello
Social platform
operationId: updatePet
requestBody:
description: Update an existent pet in the store
@@ -37,14 +37,14 @@ async function resolveOpenAPIOperationBlockNoCache(
const { context, block } = args;
if (!block.data.path || !block.data.method) {
return { data: null, specUrl: null, publicURL: null };
return { data: null, specUrl: null };
}
try {
const { filesystem, specUrl, publicURL } = await fetchOpenAPIFilesystem({ block, context });
const { filesystem, specUrl } = await fetchOpenAPIFilesystem({ block, context });
if (!filesystem) {
return { data: null, specUrl: null, publicURL: null };
return { data: null, specUrl: null };
}
const data = await resolveOpenAPIOperation(filesystem, {
@@ -52,7 +52,7 @@ async function resolveOpenAPIOperationBlockNoCache(
method: block.data.method,
});
return { data, specUrl, publicURL };
return { data, specUrl };
} catch (error) {
if (error instanceof OpenAPIParseError) {
return { error };
@@ -37,21 +37,21 @@ async function baseResolveOpenAPISchemasBlock(
): Promise<ResolveOpenAPISchemasBlockResult> {
const { context, block } = args;
if (!block.data.schemas || !block.data.schemas.length) {
return { data: null, specUrl: null, publicURL: null };
return { data: null, specUrl: null };
}
try {
const { filesystem, specUrl, publicURL } = await fetchOpenAPIFilesystem({ block, context });
const { filesystem, specUrl } = await fetchOpenAPIFilesystem({ block, context });
if (!filesystem || !specUrl) {
return { data: null, specUrl: null, publicURL: null };
return { data: null, specUrl: null };
}
const data = await resolveOpenAPISchemas(filesystem, {
schemas: block.data.schemas,
});
return { data, specUrl, publicURL };
return { data, specUrl };
} catch (error) {
if (error instanceof OpenAPIParseError) {
return { error };
@@ -35,14 +35,14 @@ async function baseResolveOpenAPIWebhookBlock(
): Promise<ResolveOpenAPIWebhookBlockResult> {
const { context, block } = args;
if (!block.data.name || !block.data.method) {
return { data: null, specUrl: null, publicURL: null };
return { data: null, specUrl: null };
}
try {
const { filesystem, specUrl, publicURL } = await fetchOpenAPIFilesystem({ block, context });
const { filesystem, specUrl } = await fetchOpenAPIFilesystem({ block, context });
if (!filesystem) {
return { data: null, specUrl: null, publicURL: null };
return { data: null, specUrl: null };
}
const data = await resolveOpenAPIWebhook(filesystem, {
@@ -50,7 +50,7 @@ async function baseResolveOpenAPIWebhookBlock(
method: block.data.method,
});
return { data, specUrl, publicURL };
return { data, specUrl };
} catch (error) {
if (error instanceof OpenAPIParseError) {
return { error };
+1 -5
View File
@@ -38,8 +38,6 @@ export type FetchOpenAPIFilesystemResult =
error?: undefined;
filesystem: Filesystem<OpenAPIV3xDocument> | null;
specUrl: string | null;
/** Public URL of the OpenAPI spec, used as the spec URL for Scalar's "Test it" modal */
publicURL: string | null;
}
| FetchOpenAPIFilesystemError;
@@ -50,14 +48,13 @@ type FetchOpenAPIFilesystemError = {
error: OpenAPIParseError;
filesystem?: undefined;
specUrl?: undefined;
publicURL?: undefined;
};
/**
* Resolved OpenAPI block result.
*/
export type ResolveOpenAPIBlockResult<T> =
| { error?: undefined; data: T | null; specUrl: string | null; publicURL: string | null }
| { error?: undefined; data: T | null; specUrl: string | null }
| ResolveOpenAPIBlockError;
/**
@@ -67,5 +64,4 @@ type ResolveOpenAPIBlockError = {
error: OpenAPIParseError;
data?: undefined;
specUrl?: undefined;
publicURL?: undefined;
};
+1 -62
View File
@@ -1,74 +1,13 @@
import type { PublishedSiteContent } from '@gitbook/api';
import assertNever from 'assert-never';
import { assert } from 'ts-essentials';
import { GITBOOK_PREVIEW_BASE_URL } from './env';
import type { ResponseCookie } from './visitors';
/**
* Check if the request to the site is a preview request.
*/
export function isPreviewRequest(requestURL: URL): boolean {
// TODO: Remove the `requestURL.host === 'preview'` condition once we have fully migrated to the new preview URL structure.
return (
requestURL.host === 'preview' || requestURL.toString().startsWith(GITBOOK_PREVIEW_BASE_URL)
);
return requestURL.host === 'preview';
}
export function getPreviewRequestIdentifier(requestURL: URL): string {
if (requestURL.toString().startsWith(GITBOOK_PREVIEW_BASE_URL)) {
const siteIdentifier = requestURL.pathname.split('/').filter(Boolean)[1];
assert(siteIdentifier, 'Expected site identifier in preview URL');
return siteIdentifier;
}
// TODO: Remove the `requestURL.host === 'preview'` condition once we have fully migrated to the new preview URL structure.
// For preview requests, we extract the site ID from the pathname
// e.g. https://preview/site_id/...
const pathname = requestURL.pathname.slice(1).split('/');
return pathname[0]!;
}
/**
* Get a cookie for the preview request.
*/
export function getPreviewCookieResponse(args: {
name: string;
value: string;
mode: 'url' | 'url-host';
siteRequestURL: URL;
siteURLData: PublishedSiteContent;
}): ResponseCookie {
const { name, value, mode, siteRequestURL, siteURLData } = args;
// Only send the cookie to preview routes and scope it to the specific site
// to avoid conflicts between different sites previews potentially opened at the same time.
const path = (() => {
switch (mode) {
case 'url': {
const gitbookPreviewBaseURL = new URL(GITBOOK_PREVIEW_BASE_URL);
const gitbookPreviewHost =
gitbookPreviewBaseURL.host + gitbookPreviewBaseURL.pathname.replace(/\/$/, '');
// TODO: Remove support for 'preview' hostnames later.
const host =
siteRequestURL.hostname === 'preview'
? siteRequestURL.hostname
: gitbookPreviewHost;
return `/url/${host}/${getPreviewRequestIdentifier(siteRequestURL)}`;
}
case 'url-host':
return siteURLData.siteBasePath;
default:
assertNever(mode);
}
})();
return {
name,
value,
options: {
httpOnly: true,
sameSite: 'lax',
maxAge: 10 * 60, // 10 minutes
path,
},
};
}
+15 -34
View File
@@ -29,12 +29,7 @@ import { PageIcon } from '@/components/PageIcon';
import { getGitBookAppHref } from './app';
import { getBlockById, getBlockTitle } from './document';
import { resolvePageId } from './pages';
import {
findSiteSpaceBy,
getFallbackSiteSpacePath,
getLocalizedTitle,
isSpaceInSiteStructure,
} from './sites';
import { findSiteSpaceBy, getFallbackSiteSpacePath } from './sites';
import { getRevisionTags, resolveTag } from './tags';
import type { ClassValue } from './tailwind';
import { filterOutNullable } from './typescript';
@@ -63,17 +58,12 @@ export interface ResolvedContentRef {
context: GitBookSpaceContext;
revisionReusableContent: RevisionReusableContent;
};
/** Resolve OpenAPI spec filesystem. */
openAPIFilesystem?: Filesystem;
/**
* Space that the content ref belongs to (if applicable).
*/
space?: Space;
/** Resolved OpenAPI spec, if the reference is an OpenAPI spec. */
openapi?: {
/** OpenAPI spec filesystem. */
filesystem: Filesystem;
/** Public URL of the OpenAPI spec */
publicURL: string | null;
};
}
export interface ResolveContentRefOptions {
@@ -122,6 +112,8 @@ export async function resolveContentRefInDocument(
}
}
console.log('resolveContentRefInDocument', contentRef, context, options);
return resolveContentRef(contentRef, context, options);
}
@@ -168,14 +160,6 @@ export async function resolveContentRef(
case 'anchor':
case 'page': {
if (isContentRefInDifferentSpace(contentRef, context)) {
// Only resolve cross-space page/anchor refs when the target space
// is part of the current site's structure.
if (
'site' in context &&
!isSpaceInSiteStructure(context.structure, contentRef.space)
) {
return null;
}
return resolveContentRefInSpace(contentRef.space, context, contentRef, options);
}
@@ -358,13 +342,13 @@ export async function resolveContentRef(
return null;
}
return {
href: openAPISpecVersionContent.urls.source,
// @ts-expect-error - Backward compatibility: `urls.source` replaces `url` in the next API version
href: openAPISpecVersionContent.urls?.source ?? openAPISpecVersionContent.url,
text: contentRef.spec,
active: false,
openapi: {
filesystem: openAPISpecVersionContent.filesystem as Filesystem,
publicURL: openAPISpecVersionContent.urls.public,
},
openAPIFilesystem: openAPISpecVersionContent.filesystem as Filesystem,
// @ts-expect-error - Public URL is not yet supported in the API
publicURL: openAPISpecVersionContent.urls?.public,
};
}
@@ -501,18 +485,15 @@ async function resolveContentRefInSpace(
// Prefer the variant title when available, then the section title, then fallback to the space title.
const ancestorLabel = (() => {
if ('site' in context) {
const currentLanguage = context.siteSpace.space.language;
const foundSiteSpace = findSiteSpaceBy(
context.structure,
(siteSpace) => siteSpace.space.id === spaceId
);
if (foundSiteSpace?.siteSpace) {
return getLocalizedTitle(foundSiteSpace.siteSpace, currentLanguage);
}
if (foundSiteSpace?.siteSection) {
return getLocalizedTitle(foundSiteSpace.siteSection, currentLanguage);
}
return ctx.spaceContext.space.title;
return (
foundSiteSpace?.siteSpace.title ??
foundSiteSpace?.siteSection?.title ??
ctx.spaceContext.space.title
);
}
return ctx.spaceContext.space.title;
+12 -35
View File
@@ -1,6 +1,5 @@
import type { GitBookSiteContext } from '@/lib/context';
import type {
LocalizedString,
SiteSection,
SiteSectionGroup,
SiteSpace,
@@ -57,14 +56,6 @@ export function listAllSiteSpaces(siteStructure: SiteStructure) {
});
}
/**
* Check if a space is part of a site structure by scanning every site section
* and all the siteSpaces associated with each section.
*/
export function isSpaceInSiteStructure(siteStructure: SiteStructure, spaceId: string): boolean {
return !!findSiteSpaceBy(siteStructure, (siteSpace) => siteSpace.space.id === spaceId);
}
/**
* Find a site space by its spaceId in a site structure.
*/
@@ -194,34 +185,20 @@ function findSiteSpaceByIdInSiteSpaces(
}
/**
* Get the localized title for a site entity (SiteSection, SiteSectionGroup, or SiteSpace).
* Get the appropriate title for a siteSection.
* Uses localizedTitle if available and current language is provided, otherwise uses title.
*/
export function getLocalizedTitle(
entity: { title: string; localizedTitle?: LocalizedString },
export function getSiteSectionTitle(
siteSection: SiteSection,
currentLanguage: TranslationLanguage | undefined
): string {
return getLocalizedField(entity.localizedTitle, currentLanguage) ?? entity.title;
}
/**
* Get the localized description for a site entity.
*/
export function getLocalizedDescription(
entity: { description?: string; localizedDescription?: LocalizedString },
currentLanguage: TranslationLanguage | undefined
): string | undefined {
return getLocalizedField(entity.localizedDescription, currentLanguage) ?? entity.description;
}
/**
* Get a localized field value for the given language.
*/
function getLocalizedField(
localizedField: LocalizedString | undefined,
currentLanguage: TranslationLanguage | undefined
): string | undefined {
if (localizedField && currentLanguage && localizedField[currentLanguage]) {
return localizedField[currentLanguage];
if (
siteSection.localizedTitle &&
currentLanguage &&
siteSection.localizedTitle[currentLanguage]
) {
return siteSection.localizedTitle[currentLanguage];
}
return undefined;
return siteSection.title;
}
+56 -67
View File
@@ -4,7 +4,6 @@ import type { NextRequest } from 'next/server';
import { NextResponse } from 'next/server';
import rison from 'rison';
import type { SiteURLData } from '@/lib/context';
import { getContentSecurityPolicy } from '@/lib/csp';
import { validateSerializedCustomization } from '@/lib/customization';
import {
@@ -18,16 +17,7 @@ import {
import { GITBOOK_OAUTH_SERVER_URL, isGitBookAssetsHostURL, isGitBookHostURL } from '@/lib/env';
import { getImageResizingContextId } from '@/lib/images';
import { MiddlewareHeaders } from '@/lib/middleware';
import {
handleUnauthedOAuthProtectedResourceRequest,
isOAuthProtectedResourceRequest,
} from '@/lib/oauth-protected';
import { removeLeadingSlash, removeTrailingSlash } from '@/lib/paths';
import {
getPreviewCookieResponse,
getPreviewRequestIdentifier,
isPreviewRequest,
} from '@/lib/preview';
import {
type ResponseCookies,
getPathScopedCookieName,
@@ -38,6 +28,12 @@ import {
} from '@/lib/visitors';
import { serveResizedImage } from '@/routes/image';
import { cookies } from 'next/headers';
import type { SiteURLData } from './lib/context';
import {
handleUnauthedOAuthProtectedResourceRequest,
isOAuthProtectedResourceRequest,
} from './lib/oauth-protected';
import { getPreviewRequestIdentifier } from './lib/preview';
import { serveProxyAnalyticsEvent } from './lib/tracking';
export const config = {
matcher: [
@@ -344,7 +340,6 @@ async function serveSiteRoutes(requestURL: URL, request: NextRequest) {
changeRequest: siteURLData.changeRequest,
revision: siteURLData.revision,
shareKey: siteURLData.shareKey,
preview: siteURLData.preview,
apiToken: siteURLData.apiToken,
imagesContextId: imagesContextId,
contextId: siteURLData.contextId,
@@ -369,17 +364,18 @@ async function serveSiteRoutes(requestURL: URL, request: NextRequest) {
routeType = 'dynamic';
// We need to encode the customization headers, otherwise it will fail for some customization values containing non ASCII chars on vercel.
requestHeaders.set(MiddlewareHeaders.Customization, encodeURIComponent(customization));
if (siteURLData.preview) {
cookies.push(
getPreviewCookieResponse({
name: MiddlewareHeaders.Customization,
value: encodeURIComponent(customization),
mode,
siteRequestURL,
siteURLData,
})
);
}
cookies.push({
name: MiddlewareHeaders.Customization,
value: encodeURIComponent(customization),
options: {
httpOnly: true,
sameSite: 'lax',
maxAge: 10 * 60, // 10 minutes
// Only send the cookie to preview routes and scope it to the specific site
// to avoid conflicts between different sites previews potentially opened at the same time.
path: `/url/preview/${getPreviewRequestIdentifier(siteRequestURL)}`,
},
});
}
const theme =
siteRequestURL.searchParams.get('theme') ??
@@ -387,17 +383,16 @@ async function serveSiteRoutes(requestURL: URL, request: NextRequest) {
if (theme === CustomizationThemeMode.Dark || theme === CustomizationThemeMode.Light) {
routeType = 'dynamic';
requestHeaders.set(MiddlewareHeaders.Theme, theme);
if (siteURLData.preview) {
cookies.push(
getPreviewCookieResponse({
name: MiddlewareHeaders.Theme,
value: theme,
mode,
siteRequestURL,
siteURLData,
})
);
}
cookies.push({
name: MiddlewareHeaders.Theme,
value: theme,
options: {
httpOnly: true,
sameSite: 'lax',
maxAge: 10 * 60, // 10 minutes
path: '/url/preview', // Only send the cookie to preview routes
},
});
}
// We support forcing dynamic routes by setting a `gitbook-dynamic-route` cookie
@@ -467,20 +462,17 @@ async function serveSiteRoutes(requestURL: URL, request: NextRequest) {
return writeResponseCookies(response, cookies);
};
// For preview requests like:
// - https://preview/<siteURL> requests, TODO: Remove support for this format later
// - https://<GITBOOK_PREVIEW_BASE_URL>/<siteID> requests (ex: https://sites.gitbook.com/preview/site_id/path)
if (isPreviewRequest(siteRequestURL)) {
// For https://preview/<siteURL> requests,
if (siteRequestURL.hostname === 'preview') {
// Do not track page views for preview requests
request.headers.set('x-gitbook-disable-tracking', 'true');
return serveWithQueryAPIToken({
return serveWithQueryAPIToken(
// We scope the API token to the site ID.
scopePath: ['preview', getPreviewRequestIdentifier(siteRequestURL)].join('/'),
// We keep the original request URL when using `url` mode
requestURL: mode === 'url' ? requestURL : siteRequestURL,
requestCookies: request.cookies,
serve: withAPIToken,
});
`${siteRequestURL.hostname}/${requestURL.pathname.slice(1).split('/')[0]}`,
request,
withAPIToken
);
}
return withAPIToken(null);
@@ -495,22 +487,18 @@ async function serveSpacePDFRoutes(requestURL: URL, request: NextRequest) {
return null;
}
return serveWithQueryAPIToken({
scopePath: pathnameParts.slice(0, 2).join('/'),
requestURL,
requestCookies: request.cookies,
serve: async (apiToken) => {
if (!apiToken) {
throw new DataFetcherError('Missing API token', 400);
}
return serveWithQueryAPIToken(
pathnameParts.slice(0, 2).join('/'),
request,
async (apiToken) => {
// Handle the rest with the router default logic
return NextResponse.next({
headers: {
[MiddlewareHeaders.APIToken]: apiToken,
},
});
},
});
}
);
}
/**
@@ -528,25 +516,23 @@ function serveErrorResponse(error: Error) {
}
/**
* Serve a response with an API token obtained from the query params.
* Server a response with an API token obtained from the query params.
*/
async function serveWithQueryAPIToken(input: {
scopePath: string;
requestURL: URL;
requestCookies: NextRequest['cookies'];
serve: (apiToken: string | null) => Promise<NextResponse>;
}) {
const { scopePath, requestURL, requestCookies, serve } = input;
async function serveWithQueryAPIToken(
scopePath: string,
request: NextRequest,
serve: (apiToken: string) => Promise<NextResponse>
) {
// We store the API token in a cookie that is scoped to the specific route
// to avoid errors when multiple previews are opened in different tabs.
const cookieName = getPathScopedCookieName('gitbook-api-token', scopePath);
// Extract a potential GitBook API token passed in the request
// If found, we redirect to the same URL but with the token in the cookie
const queryAPIToken = requestURL.searchParams.get('token');
const queryAPIToken = request.nextUrl.searchParams.get('token');
if (queryAPIToken) {
requestURL.searchParams.delete('token');
return writeResponseCookies(NextResponse.redirect(requestURL.toString()), [
request.nextUrl.searchParams.delete('token');
return writeResponseCookies(NextResponse.redirect(request.nextUrl.toString()), [
{
name: cookieName,
value: queryAPIToken,
@@ -560,9 +546,12 @@ async function serveWithQueryAPIToken(input: {
]);
}
const apiToken = requestCookies.get(cookieName)?.value;
const apiToken = request.cookies.get(cookieName)?.value;
if (!apiToken) {
throw new DataFetcherError('Missing API token', 400);
}
return serve(apiToken ?? null);
return serve(apiToken);
}
/**
+3 -8
View File
@@ -3,7 +3,7 @@ import { throwIfDataError } from '@/lib/data';
import type { GitBookLinker } from '@/lib/links';
import { joinPath } from '@/lib/paths';
import { type FlatPageEntry, getIndexablePages } from '@/lib/sitemap';
import { getLocalizedTitle, getSiteStructureSections } from '@/lib/sites';
import { getSiteSectionTitle, getSiteStructureSections } from '@/lib/sites';
import type { SiteSection, SiteSpace } from '@gitbook/api';
import assertNever from 'assert-never';
import type { ListItem, Paragraph, Root, RootContent } from 'mdast';
@@ -101,7 +101,7 @@ async function getNodesFromSections(
type: 'heading',
depth: 2,
children: [
{ type: 'text', value: getLocalizedTitle(siteSection, currentLanguage) },
{ type: 'text', value: getSiteSectionTitle(siteSection, currentLanguage) },
],
},
...siteSpaceNodes,
@@ -152,12 +152,7 @@ async function getNodesFromSiteSpaces(
nodes.push({
type: 'heading',
depth: 2,
children: [
{
type: 'text',
value: getLocalizedTitle(siteSpace, context.siteSpace.space.language),
},
],
children: [{ type: 'text', value: siteSpace.title }],
});
}
-8
View File
@@ -45,14 +45,6 @@ export function getTestURL(urlRest: string): string {
return url.toString();
}
/**
* Get the upstream GitBook preview URL for a preview route.
*/
export function getGitBookPreviewURL(urlRest: string): string {
const url = new URL(urlRest, 'https://sites.gitbook.com/preview/');
return url.href.replace(`${url.protocol}//`, '');
}
/**
* Get an API token for a site by its URL.
*/
-3
View File
@@ -7,9 +7,6 @@
"allow_importable_env",
"global_fetch_strictly_public"
],
"observability": {
"enabled": false
},
"assets": {
"directory": ".open-next/assets",
"binding": "ASSETS"
-6
View File
@@ -1,11 +1,5 @@
# @gitbook/openapi-parser
## 3.0.10
### Patch Changes
- 8ab419a: Support x-enable-proxy at operation level
## 3.0.9
### Patch Changes
+1 -1
View File
@@ -8,7 +8,7 @@
"default": "./dist/index.js"
}
},
"version": "3.0.10",
"version": "3.0.9",
"sideEffects": false,
"dependencies": {
"@scalar/json-magic": "^0.9.4",
-11
View File
@@ -1,16 +1,5 @@
# @gitbook/react-openapi
## 1.5.10
### Patch Changes
- bdc6cd9: Use OpenAPI publicURL as Scalar specUrl
- 8ab419a: Support x-enable-proxy at operation level
- eceec6f: Move ~scalar/proxy route to site route group
- 887c3f8: Add token verification on OpenAPI proxy
- Updated dependencies [8ab419a]
- @gitbook/openapi-parser@3.0.10
## 1.5.9
### Patch Changes
+1 -1
View File
@@ -7,7 +7,7 @@
"default": "./dist/index.js"
}
},
"version": "1.5.10",
"version": "1.5.9",
"sideEffects": false,
"dependencies": {
"@gitbook/expr": "workspace:*",
@@ -50,71 +50,6 @@ describe('getSecurityHeaders', () => {
});
});
it('should use default placeholder for basic auth', () => {
const securities: OpenAPIOperationData['securities'] = [
[
'basicAuth',
{
type: 'http',
scheme: 'basic',
},
],
];
const result = getSecurityHeaders({
securityRequirement: [{ basicAuth: [] }],
securities,
});
expect(result).toEqual({
Authorization: 'Basic username:password',
});
});
it('should use x-gitbook-token-placeholder for basic auth', () => {
const securities: OpenAPIOperationData['securities'] = [
[
'basicAuth',
{
type: 'http',
scheme: 'basic',
'x-gitbook-token-placeholder': 'admin:secret123',
},
],
];
const result = getSecurityHeaders({
securityRequirement: [{ basicAuth: [] }],
securities,
});
expect(result).toEqual({
Authorization: 'Basic admin:secret123',
});
});
it('should use x-gitbook-token-placeholder for bearer auth', () => {
const securities: OpenAPIOperationData['securities'] = [
[
'bearerAuth',
{
type: 'http',
scheme: 'bearer',
'x-gitbook-token-placeholder': 'MY_CUSTOM_TOKEN',
},
],
];
const result = getSecurityHeaders({
securityRequirement: [{ bearerAuth: [] }],
securities,
});
expect(result).toEqual({
Authorization: 'Bearer MY_CUSTOM_TOKEN',
});
});
it('should not use x-gitbook-prefix for http scheme', () => {
const securities: OpenAPIOperationData['securities'] = [
[
@@ -245,7 +245,7 @@ function OpenAPICodeSampleFooter(props: {
) : (
<span />
)}
{!hideTryItPanel && hasValidHost && specUrl && (
{!hideTryItPanel && hasValidHost && (
<ScalarApiButton
context={resolveScalarClientContext(context, servers, specUrl)}
withProxy={Boolean(data.operation['x-enable-proxy'] ?? data['x-enable-proxy'])}
+2 -3
View File
@@ -67,10 +67,9 @@ export interface OpenAPIContext
renderDocument: (props: { document: object }) => React.ReactNode;
/**
* Public specification URL, used by Scalar's "Test it" modal.
* When null, the "Test it" button is hidden.
* Specification URL.
*/
specUrl: string | null;
specUrl: string;
/**
* Build a signed proxy URL that restricts the proxy to specific origins.
@@ -118,144 +118,6 @@ describe('resolveTryItPrefillForOperation', () => {
});
});
it('should use x-gitbook-token-placeholder as fallback for basic auth in try-it', () => {
const operation: OpenAPIOperationData = {
path: '/users',
method: 'GET',
operation: { summary: 'List users' },
servers: [{ url: 'https://api.example.com' }],
securities: [
[
'basicAuth',
{
type: 'http',
scheme: 'basic',
'x-gitbook-token-placeholder': 'admin:secret123',
},
],
],
};
const prefillInputContext: PrefillInputContextData = {
visitor: { claims: {} },
};
const result = resolveTryItPrefillForOperation({
operation,
prefillInputContext,
});
expect(result).toEqual({
authentication: {
securitySchemes: {
basicAuth: { username: 'admin', password: 'secret123' },
},
},
});
});
it('should use x-gitbook-token-placeholder as fallback for bearer auth in try-it', () => {
const operation: OpenAPIOperationData = {
path: '/users',
method: 'GET',
operation: { summary: 'List users' },
servers: [{ url: 'https://api.example.com' }],
securities: [
[
'bearerAuth',
{
type: 'http',
scheme: 'bearer',
'x-gitbook-token-placeholder': 'my-default-token',
},
],
],
};
const prefillInputContext: PrefillInputContextData = {
visitor: { claims: {} },
};
const result = resolveTryItPrefillForOperation({
operation,
prefillInputContext,
});
expect(result).toEqual({
authentication: {
securitySchemes: {
bearerAuth: { token: 'my-default-token' },
},
},
});
});
it('should fall back to x-gitbook-token-placeholder when x-gitbook-prefill exists but prefillInputContext is null', () => {
const operation: OpenAPIOperationData = {
path: '/users',
method: 'GET',
operation: { summary: 'List users' },
servers: [{ url: 'https://api.example.com' }],
securities: [
[
'basicAuth',
{
type: 'http',
scheme: 'basic',
'x-gitbook-prefill': '{{ visitor.claims.basicAuth }}',
'x-gitbook-token-placeholder': 'admin:secret123',
},
],
],
};
const result = resolveTryItPrefillForOperation({
operation,
prefillInputContext: null,
});
expect(result).toEqual({
authentication: {
securitySchemes: {
basicAuth: { username: 'admin', password: 'secret123' },
},
},
});
});
it('should fall back to x-gitbook-token-placeholder when x-gitbook-prefill expression resolves to undefined', () => {
const operation: OpenAPIOperationData = {
path: '/users',
method: 'GET',
operation: { summary: 'List users' },
servers: [{ url: 'https://api.example.com' }],
securities: [
[
'bearerAuth',
{
type: 'http',
scheme: 'bearer',
'x-gitbook-prefill': '{{ visitor.claims.missing }}',
'x-gitbook-token-placeholder': 'fallback-token',
},
],
],
};
const result = resolveTryItPrefillForOperation({
operation,
prefillInputContext: { visitor: { claims: {} } },
});
expect(result).toEqual({
authentication: {
securitySchemes: {
bearerAuth: { token: 'fallback-token' },
},
},
});
});
it('should return empty object if no visitor data matches prefill expression', () => {
const operation: OpenAPIOperationData = {
path: '/orgs/<orgId>/spaces',
@@ -305,9 +167,7 @@ describe('resolveTryItPrefillForOperation', () => {
const prefillInputContext: PrefillInputContextData = {
visitor: {
claims: {
api: { endpointUrl: 'https://api.gitbook-staging.com/v1/' },
},
claims: { api: { endpointUrl: 'https://api.gitbook-staging.com/v1/' } },
},
};
@@ -354,9 +214,7 @@ describe('resolveTryItPrefillForOperation', () => {
// Override env
const overrideEnvResult = resolveTryItPrefillForOperation({
operation,
prefillInputContext: {
visitor: { claims: { api: { env: 'staging' } } },
},
prefillInputContext: { visitor: { claims: { api: { env: 'staging' } } } },
});
expect(overrideEnvResult).toEqual({
servers: [
@@ -374,9 +232,7 @@ describe('resolveTryItPrefillForOperation', () => {
// Override version
const overrideVersionResult = resolveTryItPrefillForOperation({
operation,
prefillInputContext: {
visitor: { claims: { api: { version: 'v2' } } },
},
prefillInputContext: { visitor: { claims: { api: { version: 'v2' } } } },
});
expect(overrideVersionResult).toEqual({
servers: [
@@ -611,10 +467,7 @@ describe('resolveURLWithPrefillCodePlaceholdersFromServer', () => {
const result = resolveURLWithPrefillCodePlaceholdersFromServer({
url: 'https://{region}.example.com',
variables: {
region: {
default: 'us-east-1',
'x-gitbook-prefill': '{{ user.region }}',
},
region: { default: 'us-east-1', 'x-gitbook-prefill': '{{ user.region }}' },
},
});
@@ -639,10 +492,7 @@ describe('resolveURLWithPrefillCodePlaceholdersFromServer', () => {
url: 'https://{region}.example.com/{version}',
'x-gitbook-prefill': '{{ user.baseUrl }}',
variables: {
region: {
default: 'us-east-1',
'x-gitbook-prefill': '{{ user.region }}',
},
region: { default: 'us-east-1', 'x-gitbook-prefill': '{{ user.region }}' },
version: { default: 'v1' },
},
});
@@ -33,18 +33,18 @@ export function resolveTryItPrefillForOperation(args: {
prefillInputContext,
} = args;
const resolveTryItPrefillExpression = prefillInputContext
? (() => {
const runtime = new ExpressionRuntime();
return (expr: string) => {
const parts = parseTemplate(expr);
if (!parts.length) {
return undefined;
}
return runtime.evaluateTemplate(expr, prefillInputContext);
};
})()
: undefined;
if (!prefillInputContext) {
return {};
}
const runtime = new ExpressionRuntime();
const resolveTryItPrefillExpression = (expr: string) => {
const parts = parseTemplate(expr);
if (!parts.length) {
return undefined;
}
return runtime.evaluateTemplate(expr, prefillInputContext);
};
const prefillAuth = securities
? resolveTryItPrefillAuthForOperationSecurities({
@@ -54,10 +54,7 @@ export function resolveTryItPrefillForOperation(args: {
: undefined;
const prefillServers = servers
? resolveTryItPrefillServersForOperationServers({
servers,
resolveTryItPrefillExpression,
})
? resolveTryItPrefillServersForOperationServers({ servers, resolveTryItPrefillExpression })
: [];
return {
@@ -71,17 +68,15 @@ export function resolveTryItPrefillForOperation(args: {
*/
function resolveTryItPrefillAuthForOperationSecurities(args: {
securities: OpenAPIOperationData['securities'];
resolveTryItPrefillExpression?: (expr: string) => string | undefined;
resolveTryItPrefillExpression: (expr: string) => string | undefined;
}): ApiClientConfiguration['authentication'] | undefined {
const { securities, resolveTryItPrefillExpression } = args;
const prefillAuthConfig: ApiClientConfiguration['authentication']['securitySchemes'] = {};
for (const [schemeName, security] of Object.values(securities)) {
const resolvedPrefill = security[PREFILL_CUSTOM_PROPERTY]
? resolveTryItPrefillExpression?.(security[PREFILL_CUSTOM_PROPERTY])
const tryitPrefillAuthValue = security[PREFILL_CUSTOM_PROPERTY]
? resolveTryItPrefillExpression(security[PREFILL_CUSTOM_PROPERTY])
: undefined;
const tryitPrefillAuthValue =
resolvedPrefill || security['x-gitbook-token-placeholder'] || undefined;
if (!tryitPrefillAuthValue) {
continue;
@@ -125,7 +120,7 @@ function resolveTryItPrefillAuthForOperationSecurities(args: {
*/
function resolveTryItPrefillServersForOperationServers(args: {
servers: OpenAPIOperationData['servers'];
resolveTryItPrefillExpression?: (expr: string) => string | undefined;
resolveTryItPrefillExpression: (expr: string) => string | undefined;
}): ApiClientConfiguration['servers'] | undefined {
const { servers, resolveTryItPrefillExpression } = args;
const resolvedServers: ApiClientConfiguration['servers'] = [];
@@ -133,10 +128,9 @@ function resolveTryItPrefillServersForOperationServers(args: {
for (const server of servers) {
// Url-level prefill
const tryItPrefillServerUrlExpr = server[PREFILL_CUSTOM_PROPERTY];
const tryItPrefillServerUrlValue =
tryItPrefillServerUrlExpr && resolveTryItPrefillExpression
? resolveTryItPrefillExpression(tryItPrefillServerUrlExpr)
: undefined;
const tryItPrefillServerUrlValue = tryItPrefillServerUrlExpr
? resolveTryItPrefillExpression(tryItPrefillServerUrlExpr)
: undefined;
const variables: { [variable: string]: OpenAPIV3.ServerVariableObject } = server.variables
? { ...server.variables }
@@ -149,7 +143,7 @@ function resolveTryItPrefillServersForOperationServers(args: {
variable;
const tryItPrefillVarValue = tryItPrefillVarExpr
? resolveTryItPrefillExpression?.(tryItPrefillVarExpr)
? resolveTryItPrefillExpression(tryItPrefillVarExpr)
: undefined;
variables[varName] = {
...variableProps,
@@ -272,7 +266,5 @@ function templatePartsToExpression(parts: ReturnType<typeof parseTemplate>) {
}
function toPrefillCodePlaceholder(expression: string, defaultValue?: string) {
return `$$__X-GITBOOK-PREFILL[(${expression})${
defaultValue ? ` ?? '${defaultValue}'` : ''
}]__$$`;
return `$$__X-GITBOOK-PREFILL[(${expression})${defaultValue ? ` ?? '${defaultValue}'` : ''}]__$$`;
}