Skip to content

Navigation Menu

Sign in
Appearance settings

Search code, repositories, users, issues, pull requests...

Provide feedback

We read every piece of feedback, and take your input very seriously.

Saved searches

Use saved searches to filter your results more quickly

Sign up
Appearance settings

Commit 47fb69f

Browse files
committed
feat: support skew protection
1 parent ece4338 commit 47fb69f

File tree

4 files changed

+450
-1
lines changed

4 files changed

+450
-1
lines changed

‎src/build/plugin-context.ts‎

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -207,6 +207,11 @@ export class PluginContext {
207207
return join(this.edgeFunctionsDir, EDGE_HANDLER_NAME)
208208
}
209209

210+
/** Absolute path to the skew protection config */
211+
get skewProtectionConfigPath(): string {
212+
return this.resolveFromPackagePath('.netlify/v1/skew-protection.json')
213+
}
214+
210215
constructor(options: NetlifyPluginOptions) {
211216
this.constants = options.constants
212217
this.featureFlags = options.featureFlags

‎src/build/skew-protection.test.ts‎

Lines changed: 330 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,330 @@
1+
import { mkdir, writeFile } from 'node:fs/promises'
2+
import { dirname } from 'node:path'
3+
4+
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'
5+
6+
import type { PluginContext } from './plugin-context.js'
7+
import { setSkewProtection, shouldEnableSkewProtection } from './skew-protection.js'
8+
9+
// Mock fs promises
10+
vi.mock('node:fs/promises', () => ({
11+
mkdir: vi.fn(),
12+
writeFile: vi.fn(),
13+
}))
14+
15+
// Mock path
16+
vi.mock('node:path', () => ({
17+
dirname: vi.fn(),
18+
}))
19+
20+
describe('shouldEnableSkewProtection', () => {
21+
let mockCtx: PluginContext
22+
let originalEnv: NodeJS.ProcessEnv
23+
24+
beforeEach(() => {
25+
// Save original env
26+
originalEnv = { ...process.env }
27+
28+
// Reset env vars
29+
delete process.env.NETLIFY_NEXT_SKEW_PROTECTION
30+
// Set valid DEPLOY_ID by default
31+
process.env.DEPLOY_ID = 'test-deploy-id'
32+
33+
mockCtx = {
34+
featureFlags: {},
35+
constants: {
36+
IS_LOCAL: false,
37+
},
38+
} as PluginContext
39+
40+
vi.clearAllMocks()
41+
})
42+
43+
afterEach(() => {
44+
// Restore original env
45+
process.env = originalEnv
46+
})
47+
48+
describe('default behavior', () => {
49+
it('should return disabled by default', () => {
50+
const result = shouldEnableSkewProtection(mockCtx)
51+
52+
expect(result).toEqual({
53+
enabled: false,
54+
enabledOrDisabledReason: 'off-default',
55+
})
56+
})
57+
})
58+
59+
describe('environment variable opt-in', () => {
60+
it('should enable when NETLIFY_NEXT_SKEW_PROTECTION is "true"', () => {
61+
process.env.NETLIFY_NEXT_SKEW_PROTECTION = 'true'
62+
63+
const result = shouldEnableSkewProtection(mockCtx)
64+
65+
expect(result).toEqual({
66+
enabled: true,
67+
enabledOrDisabledReason: 'on-env-var',
68+
})
69+
})
70+
71+
it('should enable when NETLIFY_NEXT_SKEW_PROTECTION is "1"', () => {
72+
process.env.NETLIFY_NEXT_SKEW_PROTECTION = '1'
73+
74+
const result = shouldEnableSkewProtection(mockCtx)
75+
76+
expect(result).toEqual({
77+
enabled: true,
78+
enabledOrDisabledReason: 'on-env-var',
79+
})
80+
})
81+
})
82+
83+
describe('environment variable opt-out', () => {
84+
it('should disable when NETLIFY_NEXT_SKEW_PROTECTION is "false"', () => {
85+
process.env.NETLIFY_NEXT_SKEW_PROTECTION = 'false'
86+
87+
const result = shouldEnableSkewProtection(mockCtx)
88+
89+
expect(result).toEqual({
90+
enabled: false,
91+
enabledOrDisabledReason: 'off-env-var',
92+
})
93+
})
94+
95+
it('should disable when NETLIFY_NEXT_SKEW_PROTECTION is "0"', () => {
96+
process.env.NETLIFY_NEXT_SKEW_PROTECTION = '0'
97+
98+
const result = shouldEnableSkewProtection(mockCtx)
99+
100+
expect(result).toEqual({
101+
enabled: false,
102+
enabledOrDisabledReason: 'off-env-var',
103+
})
104+
})
105+
})
106+
107+
describe('feature flag opt-in', () => {
108+
it('should enable when feature flag is set', () => {
109+
mockCtx.featureFlags = { 'next-runtime-skew-protection': true }
110+
111+
const result = shouldEnableSkewProtection(mockCtx)
112+
113+
expect(result).toEqual({
114+
enabled: true,
115+
enabledOrDisabledReason: 'on-ff',
116+
})
117+
})
118+
119+
it('should not enable when feature flag is false', () => {
120+
mockCtx.featureFlags = { 'next-runtime-skew-protection': false }
121+
122+
const result = shouldEnableSkewProtection(mockCtx)
123+
124+
expect(result).toEqual({
125+
enabled: false,
126+
enabledOrDisabledReason: 'off-default',
127+
})
128+
})
129+
})
130+
131+
describe('DEPLOY_ID validation', () => {
132+
it('should disable when DEPLOY_ID is missing and not explicitly opted in', () => {
133+
mockCtx.featureFlags = { 'next-runtime-skew-protection': true }
134+
delete process.env.DEPLOY_ID
135+
136+
const result = shouldEnableSkewProtection(mockCtx)
137+
138+
expect(result).toEqual({
139+
enabled: false,
140+
enabledOrDisabledReason: 'off-no-valid-deploy-id',
141+
})
142+
})
143+
144+
it('should disable when DEPLOY_ID is "0" and not explicitly opted in', () => {
145+
mockCtx.featureFlags = { 'next-runtime-skew-protection': true }
146+
process.env.DEPLOY_ID = '0'
147+
148+
const result = shouldEnableSkewProtection(mockCtx)
149+
150+
expect(result).toEqual({
151+
enabled: false,
152+
enabledOrDisabledReason: 'off-no-valid-deploy-id',
153+
})
154+
})
155+
156+
it('should show specific reason when env var is set but DEPLOY_ID is invalid in local context', () => {
157+
process.env.NETLIFY_NEXT_SKEW_PROTECTION = 'true'
158+
process.env.DEPLOY_ID = '0'
159+
mockCtx.constants.IS_LOCAL = true
160+
161+
const result = shouldEnableSkewProtection(mockCtx)
162+
163+
expect(result).toEqual({
164+
enabled: false,
165+
enabledOrDisabledReason: 'off-no-valid-deploy-id-env-var',
166+
})
167+
})
168+
})
169+
170+
describe('precedence', () => {
171+
it('should prioritize env var opt-out over feature flag', () => {
172+
process.env.NETLIFY_NEXT_SKEW_PROTECTION = 'false'
173+
mockCtx.featureFlags = { 'next-runtime-skew-protection': true }
174+
175+
const result = shouldEnableSkewProtection(mockCtx)
176+
177+
expect(result).toEqual({
178+
enabled: false,
179+
enabledOrDisabledReason: 'off-env-var',
180+
})
181+
})
182+
183+
it('should prioritize env var opt-in over feature flag', () => {
184+
process.env.NETLIFY_NEXT_SKEW_PROTECTION = 'true'
185+
mockCtx.featureFlags = { 'next-runtime-skew-protection': false }
186+
187+
const result = shouldEnableSkewProtection(mockCtx)
188+
189+
expect(result).toEqual({
190+
enabled: true,
191+
enabledOrDisabledReason: 'on-env-var',
192+
})
193+
})
194+
})
195+
})
196+
197+
describe('setSkewProtection', () => {
198+
let mockCtx: PluginContext
199+
let mockSpan: any
200+
let originalEnv: NodeJS.ProcessEnv
201+
let consoleSpy: any
202+
203+
beforeEach(() => {
204+
// Save original env
205+
originalEnv = { ...process.env }
206+
207+
// Reset env vars
208+
delete process.env.NETLIFY_NEXT_SKEW_PROTECTION
209+
delete process.env.NEXT_DEPLOYMENT_ID
210+
// Set valid DEPLOY_ID by default
211+
process.env.DEPLOY_ID = 'test-deploy-id'
212+
213+
mockCtx = {
214+
featureFlags: {},
215+
constants: {
216+
IS_LOCAL: false,
217+
},
218+
skewProtectionConfigPath: '/test/path/skew-protection.json',
219+
} as PluginContext
220+
221+
mockSpan = {
222+
setAttribute: vi.fn(),
223+
}
224+
225+
consoleSpy = {
226+
log: vi.spyOn(console, 'log').mockImplementation(() => {}),
227+
warn: vi.spyOn(console, 'warn').mockImplementation(() => {}),
228+
}
229+
230+
vi.clearAllMocks()
231+
})
232+
233+
afterEach(() => {
234+
// Restore original env
235+
process.env = originalEnv
236+
consoleSpy.log.mockRestore()
237+
consoleSpy.warn.mockRestore()
238+
})
239+
240+
it('should set span attribute and return early when disabled', async () => {
241+
await setSkewProtection(mockCtx, mockSpan)
242+
243+
expect(mockSpan.setAttribute).toHaveBeenCalledWith('skewProtection', 'off-default')
244+
expect(mkdir).not.toHaveBeenCalled()
245+
expect(writeFile).not.toHaveBeenCalled()
246+
expect(consoleSpy.log).not.toHaveBeenCalled()
247+
expect(consoleSpy.warn).not.toHaveBeenCalled()
248+
})
249+
250+
it('should show warning when env var is set but no valid DEPLOY_ID', async () => {
251+
process.env.NETLIFY_NEXT_SKEW_PROTECTION = 'true'
252+
process.env.DEPLOY_ID = '0'
253+
mockCtx.constants.IS_LOCAL = true
254+
255+
await setSkewProtection(mockCtx, mockSpan)
256+
257+
expect(mockSpan.setAttribute).toHaveBeenCalledWith(
258+
'skewProtection',
259+
'off-no-valid-deploy-id-env-var',
260+
)
261+
expect(consoleSpy.warn).toHaveBeenCalledWith(
262+
'NETLIFY_NEXT_SKEW_PROTECTION environment variable is set to true, but skew protection is currently unavailable for CLI deploys. Skew protection will not be enabled.',
263+
)
264+
expect(mkdir).not.toHaveBeenCalled()
265+
expect(writeFile).not.toHaveBeenCalled()
266+
})
267+
268+
it('should set up skew protection when enabled via env var', async () => {
269+
process.env.NETLIFY_NEXT_SKEW_PROTECTION = 'true'
270+
271+
vi.mocked(dirname).mockReturnValue('/test/path')
272+
273+
await setSkewProtection(mockCtx, mockSpan)
274+
275+
expect(mockSpan.setAttribute).toHaveBeenCalledWith('skewProtection', 'on-env-var')
276+
expect(consoleSpy.log).toHaveBeenCalledWith(
277+
'Setting up Next.js Skew Protection due to NETLIFY_NEXT_SKEW_PROTECTION=true environment variable.',
278+
)
279+
expect(process.env.NEXT_DEPLOYMENT_ID).toBe('test-deploy-id')
280+
expect(mkdir).toHaveBeenCalledWith('/test/path', { recursive: true })
281+
expect(writeFile).toHaveBeenCalledWith(
282+
'/test/path/skew-protection.json',
283+
JSON.stringify(
284+
{
285+
patterns: ['.*'],
286+
sources: [
287+
{
288+
type: 'cookie',
289+
name: '__vdpl',
290+
},
291+
{
292+
type: 'header',
293+
name: 'X-Deployment-Id',
294+
},
295+
{
296+
type: 'query',
297+
name: 'dpl',
298+
},
299+
],
300+
},
301+
null,
302+
2,
303+
),
304+
)
305+
})
306+
307+
it('should set up skew protection when enabled via feature flag', async () => {
308+
mockCtx.featureFlags = { 'next-runtime-skew-protection': true }
309+
310+
vi.mocked(dirname).mockReturnValue('/test/path')
311+
312+
await setSkewProtection(mockCtx, mockSpan)
313+
314+
expect(mockSpan.setAttribute).toHaveBeenCalledWith('skewProtection', 'on-ff')
315+
expect(consoleSpy.log).toHaveBeenCalledWith('Setting up Next.js Skew Protection.')
316+
expect(process.env.NEXT_DEPLOYMENT_ID).toBe('test-deploy-id')
317+
expect(mkdir).toHaveBeenCalledWith('/test/path', { recursive: true })
318+
expect(writeFile).toHaveBeenCalledWith('/test/path/skew-protection.json', expect.any(String))
319+
})
320+
321+
it('should handle different env var values correctly', async () => {
322+
process.env.NETLIFY_NEXT_SKEW_PROTECTION = '1'
323+
324+
await setSkewProtection(mockCtx, mockSpan)
325+
326+
expect(consoleSpy.log).toHaveBeenCalledWith(
327+
'Setting up Next.js Skew Protection due to NETLIFY_NEXT_SKEW_PROTECTION=1 environment variable.',
328+
)
329+
})
330+
})

0 commit comments

Comments
(0)

AltStyle によって変換されたページ (->オリジナル) /