diff --git a/src/build/skew-protection.test.ts b/src/build/skew-protection.test.ts index 0fbc2d206a..ccb5e076ad 100644 --- a/src/build/skew-protection.test.ts +++ b/src/build/skew-protection.test.ts @@ -33,6 +33,7 @@ describe('shouldEnableSkewProtection', () => { // Reset env vars delete process.env.NETLIFY_NEXT_SKEW_PROTECTION + delete process.env.NETLIFY_SKEW_PROTECTION_TOKEN // Set valid DEPLOY_ID by default process.env.DEPLOY_ID = 'test-deploy-id' @@ -72,6 +73,7 @@ describe('shouldEnableSkewProtection', () => { expect(result).toEqual({ enabled: true, enabledOrDisabledReason: EnabledOrDisabledReason.OPT_IN_ENV_VAR, + token: 'test-deploy-id', }) }) @@ -83,6 +85,7 @@ describe('shouldEnableSkewProtection', () => { expect(result).toEqual({ enabled: true, enabledOrDisabledReason: EnabledOrDisabledReason.OPT_IN_ENV_VAR, + token: 'test-deploy-id', }) }) }) @@ -121,6 +124,7 @@ describe('shouldEnableSkewProtection', () => { expect(result).toEqual({ enabled: true, enabledOrDisabledReason: EnabledOrDisabledReason.OPT_IN_FF, + token: 'test-deploy-id', }) }) @@ -146,6 +150,7 @@ describe('shouldEnableSkewProtection', () => { expect(result).toEqual({ enabled: false, enabledOrDisabledReason: EnabledOrDisabledReason.OPT_OUT_NO_VALID_DEPLOY_ID, + token: undefined, }) }) @@ -158,6 +163,7 @@ describe('shouldEnableSkewProtection', () => { expect(result).toEqual({ enabled: false, enabledOrDisabledReason: EnabledOrDisabledReason.OPT_OUT_NO_VALID_DEPLOY_ID, + token: '0', }) }) @@ -171,6 +177,7 @@ describe('shouldEnableSkewProtection', () => { expect(result).toEqual({ enabled: false, enabledOrDisabledReason: EnabledOrDisabledReason.OPT_OUT_NO_VALID_DEPLOY_ID_ENV_VAR, + token: '0', }) }) }) @@ -197,6 +204,50 @@ describe('shouldEnableSkewProtection', () => { expect(result).toEqual({ enabled: true, enabledOrDisabledReason: EnabledOrDisabledReason.OPT_IN_ENV_VAR, + token: 'test-deploy-id', + }) + }) + }) + + describe('NETLIFY_SKEW_PROTECTION_TOKEN handling', () => { + it('should prefer NETLIFY_SKEW_PROTECTION_TOKEN over DEPLOY_ID', () => { + process.env.NETLIFY_NEXT_SKEW_PROTECTION = 'true' + process.env.NETLIFY_SKEW_PROTECTION_TOKEN = 'custom-token' + process.env.DEPLOY_ID = 'deploy-id' + + const result = shouldEnableSkewProtection(mockCtx) + + expect(result).toEqual({ + enabled: true, + enabledOrDisabledReason: EnabledOrDisabledReason.OPT_IN_ENV_VAR, + token: 'custom-token', + }) + }) + + it('should fall back to DEPLOY_ID when NETLIFY_SKEW_PROTECTION_TOKEN is not set', () => { + process.env.NETLIFY_NEXT_SKEW_PROTECTION = 'true' + delete process.env.NETLIFY_SKEW_PROTECTION_TOKEN + process.env.DEPLOY_ID = 'deploy-id' + + const result = shouldEnableSkewProtection(mockCtx) + + expect(result).toEqual({ + enabled: true, + enabledOrDisabledReason: EnabledOrDisabledReason.OPT_IN_ENV_VAR, + token: 'deploy-id', + }) + }) + + it('should use NETLIFY_SKEW_PROTECTION_TOKEN with feature flag', () => { + mockCtx.featureFlags = { 'next-runtime-skew-protection': true } + process.env.NETLIFY_SKEW_PROTECTION_TOKEN = 'ff-custom-token' + + const result = shouldEnableSkewProtection(mockCtx) + + expect(result).toEqual({ + enabled: true, + enabledOrDisabledReason: EnabledOrDisabledReason.OPT_IN_FF, + token: 'ff-custom-token', }) }) }) @@ -217,6 +268,7 @@ describe('setSkewProtection', () => { // Reset env vars delete process.env.NETLIFY_NEXT_SKEW_PROTECTION + delete process.env.NETLIFY_SKEW_PROTECTION_TOKEN delete process.env.NEXT_DEPLOYMENT_ID // Set valid DEPLOY_ID by default process.env.DEPLOY_ID = 'test-deploy-id' @@ -331,4 +383,20 @@ describe('setSkewProtection', () => { 'Setting up Next.js Skew Protection due to NETLIFY_NEXT_SKEW_PROTECTION=1 environment variable.', ) }) + + it('should use NETLIFY_SKEW_PROTECTION_TOKEN when available', async () => { + process.env.NETLIFY_NEXT_SKEW_PROTECTION = 'true' + process.env.NETLIFY_SKEW_PROTECTION_TOKEN = 'custom-skew-token' + process.env.DEPLOY_ID = 'deploy-id' + + vi.mocked(dirname).mockReturnValue('/test/path') + + await setSkewProtection(mockCtx, mockSpan) + + expect(process.env.NEXT_DEPLOYMENT_ID).toBe('custom-skew-token') + expect(mockSpan.setAttribute).toHaveBeenCalledWith( + 'skewProtection', + EnabledOrDisabledReason.OPT_IN_ENV_VAR, + ) + }) }) diff --git a/src/build/skew-protection.ts b/src/build/skew-protection.ts index 150ad15f39..503903342f 100644 --- a/src/build/skew-protection.ts +++ b/src/build/skew-protection.ts @@ -63,10 +63,10 @@ export function shouldEnableSkewProtection(ctx: PluginContext) { } } - if ( - (!process.env.DEPLOY_ID || process.env.DEPLOY_ID === '0') && - optInOptions.has(enabledOrDisabledReason) - ) { + // For compatibility with old environments, fall back to the raw deploy ID. + const token = process.env.NETLIFY_SKEW_PROTECTION_TOKEN || process.env.DEPLOY_ID + + if ((!token || token === '0') && optInOptions.has(enabledOrDisabledReason)) { // We can't proceed without a valid DEPLOY_ID, because Next.js does inline deploy ID at build time // This should only be the case for CLI deploys return { @@ -78,17 +78,19 @@ export function shouldEnableSkewProtection(ctx: PluginContext) { : // this is silent disablement to avoid spam logs for users opted in via feature flag // that don't explicitly opt in via env var EnabledOrDisabledReason.OPT_OUT_NO_VALID_DEPLOY_ID, + token, } } return { enabled: optInOptions.has(enabledOrDisabledReason), enabledOrDisabledReason, + token: token as string, } } export const setSkewProtection = async (ctx: PluginContext, span: Span) => { - const { enabled, enabledOrDisabledReason } = shouldEnableSkewProtection(ctx) + const { enabled, enabledOrDisabledReason, token } = shouldEnableSkewProtection(ctx) span.setAttribute('skewProtection', enabledOrDisabledReason) @@ -109,7 +111,7 @@ export const setSkewProtection = async (ctx: PluginContext, span: Span) => { console.log('Setting up Next.js Skew Protection.') } - process.env.NEXT_DEPLOYMENT_ID = process.env.DEPLOY_ID + process.env.NEXT_DEPLOYMENT_ID = token await mkdir(dirname(ctx.skewProtectionConfigPath), { recursive: true,