diff --git a/lib/app-router/handlers/enable-draft.spec.ts b/lib/app-router/handlers/enable-draft.spec.ts index d1f7907..4b4f698 100644 --- a/lib/app-router/handlers/enable-draft.spec.ts +++ b/lib/app-router/handlers/enable-draft.spec.ts @@ -105,6 +105,24 @@ describe('handler', () => { expect(result).toHaveProperty('status', 401); }); + describe('when a x-contentful-preview-secret is provided as a query param', () => { + beforeEach(() => { + vi.stubEnv('VERCEL_AUTOMATION_BYPASS_SECRET', ''); + vi.stubEnv('CONTENTFUL_PREVIEW_SECRET', bypassToken); + const url = `https://vercel-app-router-integrations-ll9uxwb4f.vercel.app/api/enable-draft?path=%2Fblogs%2Fmy-cat&x-contentful-preview-secret=${bypassToken}`; + request = new NextRequest(url); + }); + + it('redirects safely to the provided path and DOES NOT pass through the token and bypass cookie query params', async () => { + const result = await GET(request); + expect(result).to.be.undefined; + expect(draftModeMock.enable).toHaveBeenCalled(); + expect(vi.mocked(redirect)).toHaveBeenCalledWith( + `https://vercel-app-router-integrations-ll9uxwb4f.vercel.app/blogs/my-cat`, + ); + }); + }); + describe('when a x-vercel-protection-bypass token is provided as a query param', () => { beforeEach(() => { const url = `https://vercel-app-router-integrations-ll9uxwb4f.vercel.app/api/enable-draft?path=%2Fblogs%2Fmy-cat&x-vercel-protection-bypass=${bypassToken}`; diff --git a/lib/app-router/handlers/enable-draft.ts b/lib/app-router/handlers/enable-draft.ts index 8a48f20..692548f 100644 --- a/lib/app-router/handlers/enable-draft.ts +++ b/lib/app-router/handlers/enable-draft.ts @@ -13,6 +13,7 @@ export async function enableDraftHandler( path, host, bypassToken: bypassTokenFromQuery, + contentfulPreviewSecret: contentfulPreviewSecretFromQuery, } = parseRequestUrl(request.url); // if we're in development, we don't need to check for a bypass token, and we can just enable draft mode @@ -31,6 +32,9 @@ export async function enableDraftHandler( if (bypassTokenFromQuery) { bypassToken = bypassTokenFromQuery; aud = host; + } else if (contentfulPreviewSecretFromQuery) { + bypassToken = contentfulPreviewSecretFromQuery; + aud = host; } else { // if we don't have a bypass token from the query we fall back to the _vercel_jwt cookie to find // the correct authorization bypass elements @@ -53,7 +57,9 @@ export async function enableDraftHandler( aud = vercelJwt.aud; } - if (bypassToken !== process.env.VERCEL_AUTOMATION_BYPASS_SECRET) { + // certain Vercel account tiers may not have a VERCEL_AUTOMATION_BYPASS_SECRET, so we fallback to checking the value against the CONTENTFUL_PREVIEW_SECRET + // env var, which is supported as a workaround for these accounts + if ((bypassToken !== process.env.VERCEL_AUTOMATION_BYPASS_SECRET) && (contentfulPreviewSecretFromQuery !== process.env.CONTENTFUL_PREVIEW_SECRET)) { return new Response( 'The bypass token you are authorized with does not match the bypass secret for this deployment. You might need to redeploy or go back and try the link again.', { status: 403 }, diff --git a/lib/pages-router/handlers/enable-draft.spec.ts b/lib/pages-router/handlers/enable-draft.spec.ts index 2968436..afe4ed9 100644 --- a/lib/pages-router/handlers/enable-draft.spec.ts +++ b/lib/pages-router/handlers/enable-draft.spec.ts @@ -114,6 +114,25 @@ describe('handler', () => { expect(apiResponseSpy.send).toHaveBeenCalledWith(expect.any(String)); }); + describe('when a x-contentful-preview-secret is provided as a query param', () => { + beforeEach(() => { + vi.stubEnv('VERCEL_AUTOMATION_BYPASS_SECRET', ''); + vi.stubEnv('CONTENTFUL_PREVIEW_SECRET', bypassToken); + const url = `https://vercel-app-router-integrations-ll9uxwb4f.vercel.app/api/enable-draft?path=%2Fblogs%2Fmy-cat&x-contentful-preview-secret=${bypassToken}`; + request = makeNextApiRequest(url); + }); + + it('redirects safely to the provided path and DOES NOT passes through the token and bypass cookie query params', async () => { + const result = await handler(request, response); + expect(result).to.be.undefined; + expect(apiResponseSpy.setDraftMode).toHaveBeenCalledWith({ enable: true }); + expect(apiResponseSpy.redirect).toHaveBeenCalledWith( + `https://vercel-app-router-integrations-ll9uxwb4f.vercel.app/blogs/my-cat`, + ); + }); + }); + + describe('when a x-vercel-protection-bypass token is provided as a query param', () => { beforeEach(() => { const url = `https://vercel-app-router-integrations-ll9uxwb4f.vercel.app/api/enable-draft?path=%2Fblogs%2Fmy-cat&x-vercel-protection-bypass=${bypassToken}`; diff --git a/lib/pages-router/handlers/enable-draft.ts b/lib/pages-router/handlers/enable-draft.ts index 07883ef..cb389fe 100644 --- a/lib/pages-router/handlers/enable-draft.ts +++ b/lib/pages-router/handlers/enable-draft.ts @@ -12,6 +12,7 @@ export const enableDraftHandler: NextApiHandler = async ( path, host, bypassToken: bypassTokenFromQuery, + contentfulPreviewSecret: contentfulPreviewSecretFromQuery, } = parseNextApiRequest(request); // if we're in development, we don't need to check for a bypass token, and we can just enable draft mode @@ -29,6 +30,9 @@ export const enableDraftHandler: NextApiHandler = async ( if (bypassTokenFromQuery) { bypassToken = bypassTokenFromQuery; aud = host; + } else if (contentfulPreviewSecretFromQuery) { + bypassToken = contentfulPreviewSecretFromQuery; + aud = host; } else { // if x-vercel-protection-bypass not provided in query, we defer to parsing the _vercel_jwt cookie // which bundlees the bypass token value in its payload @@ -52,7 +56,9 @@ export const enableDraftHandler: NextApiHandler = async ( aud = vercelJwt.aud; } - if (bypassToken !== process.env.VERCEL_AUTOMATION_BYPASS_SECRET) { + // certain Vercel account tiers may not have a VERCEL_AUTOMATION_BYPASS_SECRET, so we fallback to checking the value against the CONTENTFUL_PREVIEW_SECRET + // env var, which is supported as a workaround for these accounts + if ((bypassToken !== process.env.VERCEL_AUTOMATION_BYPASS_SECRET) && (contentfulPreviewSecretFromQuery !== process.env.CONTENTFUL_PREVIEW_SECRET)) { response.status(403).send( 'The bypass token you are authorized with does not match the bypass secret for this deployment. You might need to redeploy or go back and try the link again.' ) diff --git a/lib/utils/url.spec.ts b/lib/utils/url.spec.ts index 60b2048..4cb8578 100644 --- a/lib/utils/url.spec.ts +++ b/lib/utils/url.spec.ts @@ -2,7 +2,7 @@ import { describe, expect, it } from 'vitest'; import { buildRedirectUrl, parseNextApiRequest, parseRequestUrl } from './url'; import { makeNextApiRequest } from '../../test/helpers'; -const requestUrl = `https://my.vercel.app/api/enable-draft?path=%2Fblogs%2Fmy-cat&x-vercel-protection-bypass=foo`; +const requestUrl = `https://my.vercel.app/api/enable-draft?path=%2Fblogs%2Fmy-cat&x-vercel-protection-bypass=foo&x-contentful-preview-secret=bar`; describe('parseNextApiRequest', () => { const request = makeNextApiRequest(requestUrl); @@ -12,6 +12,7 @@ describe('parseNextApiRequest', () => { expect(result).toHaveProperty('origin', 'https://my.vercel.app') expect(result).toHaveProperty('host', 'my.vercel.app') expect(result).toHaveProperty('bypassToken', 'foo') + expect(result).toHaveProperty('contentfulPreviewSecret', 'bar') expect(result).toHaveProperty('path', '/blogs/my-cat') }) }) diff --git a/lib/utils/url.ts b/lib/utils/url.ts index 8b0171a..1159fe9 100644 --- a/lib/utils/url.ts +++ b/lib/utils/url.ts @@ -5,6 +5,7 @@ interface ParsedRequestUrl { host: string; path: string; bypassToken: string; + contentfulPreviewSecret: string; } export const parseNextApiRequest = ( @@ -16,8 +17,7 @@ export const parseNextApiRequest = ( const protocol = request.headers['x-forwarded-proto'] || 'https' const requestUrl = request.url && new URL(request.url, `${protocol}://${hostHeader}`).toString() - const { origin, path, host, bypassToken } = parseRequestUrl(requestUrl) - return { origin, path, host, bypassToken }; + return parseRequestUrl(requestUrl) } export const parseRequestUrl = ( @@ -28,12 +28,13 @@ export const parseRequestUrl = ( const rawPath = searchParams.get('path') || ''; const bypassToken = searchParams.get('x-vercel-protection-bypass') || ''; + const contentfulPreviewSecret = searchParams.get('x-contentful-preview-secret') || ''; // to allow query parameters to be passed through to the redirected URL, the original `path` should already be // URI encoded, and thus must be decoded here const path = decodeURIComponent(rawPath); - return { origin, path, host, bypassToken }; + return { origin, path, host, bypassToken, contentfulPreviewSecret }; }; export const buildRedirectUrl = ({