From 8c72d75b6533e97344c6767e9bacebb52d384c34 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Eduardo=20Bou=C3=A7as?= Date: Fri, 11 Jul 2025 10:28:08 +0100 Subject: [PATCH 1/3] feat: add `headers` to edge functions manifest validation --- .../manifest/__snapshots__/index.test.ts.snap | 66 +++++++++++ .../node/validation/manifest/index.test.ts | 111 ++++++++++++++++++ .../node/validation/manifest/schema.ts | 31 +++++ 3 files changed, 208 insertions(+) diff --git a/packages/edge-bundler/node/validation/manifest/__snapshots__/index.test.ts.snap b/packages/edge-bundler/node/validation/manifest/__snapshots__/index.test.ts.snap index 47d5ffdf1d..fca5f2298f 100644 --- a/packages/edge-bundler/node/validation/manifest/__snapshots__/index.test.ts.snap +++ b/packages/edge-bundler/node/validation/manifest/__snapshots__/index.test.ts.snap @@ -53,6 +53,72 @@ REQUIRED must have required property 'format' 6 | ],] `; +exports[`headers > should throw on additional property in headers 1`] = ` +[ManifestValidationError: Validation of Edge Functions manifest failed +ADDTIONAL PROPERTY must NOT have additional properties + + 33 | "x-custom-header": { + 34 | "style": "exists", +> 35 | "foo": "bar" + | ^^^^^ 😲 foo is not expected to be here! + 36 | } + 37 | } + 38 | }] +`; + +exports[`headers > should throw on invalid pattern format 1`] = ` +[ManifestValidationError: Validation of Edge Functions manifest failed +FORMAT must match format "regexPattern" + + 33 | "x-custom-header": { + 34 | "style": "regex", +> 35 | "pattern": "/^Bearer .+/" + | ^^^^^^^^^^^^^^ 👈đŸŊ format must match format "regexPattern" + 36 | } + 37 | } + 38 | }] +`; + +exports[`headers > should throw on invalid style value 1`] = ` +[ManifestValidationError: Validation of Edge Functions manifest failed +ENUM must be equal to one of the allowed values +(exists, missing, regex) + + 32 | "headers": { + 33 | "x-custom-header": { +> 34 | "style": "invalid" + | ^^^^^^^^^ 👈đŸŊ Unexpected value, should be equal to one of the allowed values + 35 | } + 36 | } + 37 | }] +`; + +exports[`headers > should throw on missing style property 1`] = ` +[ManifestValidationError: Validation of Edge Functions manifest failed +REQUIRED must have required property 'style' + + 31 | "bundler_version": "1.6.0", + 32 | "headers": { +> 33 | "x-custom-header": { + | ^ â˜šī¸ style is missing here! + 34 | "pattern": "^Bearer .+" + 35 | } + 36 | }] +`; + +exports[`headers > should throw when style is regex but pattern is missing 1`] = ` +[ManifestValidationError: Validation of Edge Functions manifest failed +REQUIRED must have required property 'pattern' + + 31 | "bundler_version": "1.6.0", + 32 | "headers": { +> 33 | "x-custom-header": { + | ^ â˜šī¸ pattern is missing here! + 34 | "style": "regex" + 35 | } + 36 | }] +`; + exports[`import map URL > should throw on wrong type 1`] = ` [ManifestValidationError: Validation of Edge Functions manifest failed TYPE must be string diff --git a/packages/edge-bundler/node/validation/manifest/index.test.ts b/packages/edge-bundler/node/validation/manifest/index.test.ts index 6a594799bd..0728247695 100644 --- a/packages/edge-bundler/node/validation/manifest/index.test.ts +++ b/packages/edge-bundler/node/validation/manifest/index.test.ts @@ -180,3 +180,114 @@ describe('import map URL', () => { expect(() => validateManifest(manifest)).toThrowErrorMatchingSnapshot() }) }) + +describe('headers', () => { + test('should accept valid headers with exists style', () => { + const manifest = getBaseManifest() + manifest.headers = { + 'x-custom-header': { + style: 'exists' + } + } + + expect(() => validateManifest(manifest)).not.toThrowError() + }) + + test('should accept valid headers with missing style', () => { + const manifest = getBaseManifest() + manifest.headers = { + 'x-custom-header': { + style: 'missing' + } + } + + expect(() => validateManifest(manifest)).not.toThrowError() + }) + + test('should accept valid headers with regex style and pattern', () => { + const manifest = getBaseManifest() + manifest.headers = { + 'x-custom-header': { + style: 'regex', + pattern: '^Bearer .+$' + } + } + + expect(() => validateManifest(manifest)).not.toThrowError() + }) + + test('should throw on missing style property', () => { + const manifest = getBaseManifest() + manifest.headers = { + 'x-custom-header': { + pattern: '^Bearer .+' + } + } + + expect(() => validateManifest(manifest)).toThrowErrorMatchingSnapshot() + }) + + test('should throw on invalid style value', () => { + const manifest = getBaseManifest() + manifest.headers = { + 'x-custom-header': { + style: 'invalid' + } + } + + expect(() => validateManifest(manifest)).toThrowErrorMatchingSnapshot() + }) + + test('should throw when style is regex but pattern is missing', () => { + const manifest = getBaseManifest() + manifest.headers = { + 'x-custom-header': { + style: 'regex' + } + } + + expect(() => validateManifest(manifest)).toThrowErrorMatchingSnapshot() + }) + + test('should throw on invalid pattern format', () => { + const manifest = getBaseManifest() + manifest.headers = { + 'x-custom-header': { + style: 'regex', + pattern: '/^Bearer .+/' + } + } + + expect(() => validateManifest(manifest)).toThrowErrorMatchingSnapshot() + }) + + test('should throw on additional property in headers', () => { + const manifest = getBaseManifest() + manifest.headers = { + 'x-custom-header': { + style: 'exists', + foo: 'bar' + } + } + + expect(() => validateManifest(manifest)).toThrowErrorMatchingSnapshot() + }) + + test('should accept multiple headers with different styles', () => { + const manifest = getBaseManifest() + manifest.headers = { + 'x-exists-header': { + style: 'exists' + }, + 'x-missing-header': { + style: 'missing' + }, + 'authorization': { + style: 'regex', + pattern: '^Bearer [a-zA-Z0-9]+$' + } + } + + expect(() => validateManifest(manifest)).not.toThrowError() + }) +}) diff --git a/packages/edge-bundler/node/validation/manifest/schema.ts b/packages/edge-bundler/node/validation/manifest/schema.ts index d244f4999d..0331d8576d 100644 --- a/packages/edge-bundler/node/validation/manifest/schema.ts +++ b/packages/edge-bundler/node/validation/manifest/schema.ts @@ -60,6 +60,36 @@ const layersSchema = { additionalProperties: false, } +const headersSchema = { + type: 'object', + patternProperties: { + '.*': { + type: 'object', + required: ['style'], + properties: { + pattern: { + type: 'string', + format: 'regexPattern', + }, + style: { + type: 'string', + enum: ['exists', 'missing', 'regex'], + }, + }, + additionalProperties: false, + if: { + properties: { + style: { const: 'regex' }, + }, + }, + then: { + required: ['pattern'], + }, + }, + }, + additionalProperties: false, +} + const edgeManifestSchema = { type: 'object', required: ['bundles', 'routes', 'bundler_version'], @@ -83,6 +113,7 @@ const edgeManifestSchema = { import_map: { type: 'string' }, bundler_version: { type: 'string' }, function_config: { type: 'object', additionalProperties: functionConfigSchema }, + headers: headersSchema, }, additionalProperties: false, } From 5bf6f517dbcc7c73af121e40d687b913b161ce86 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Eduardo=20Bou=C3=A7as?= Date: Fri, 11 Jul 2025 10:30:05 +0100 Subject: [PATCH 2/3] chore: formatting --- .../node/validation/manifest/index.test.ts | 42 +++++++++---------- 1 file changed, 21 insertions(+), 21 deletions(-) diff --git a/packages/edge-bundler/node/validation/manifest/index.test.ts b/packages/edge-bundler/node/validation/manifest/index.test.ts index 0728247695..3da72d7ad8 100644 --- a/packages/edge-bundler/node/validation/manifest/index.test.ts +++ b/packages/edge-bundler/node/validation/manifest/index.test.ts @@ -186,8 +186,8 @@ describe('headers', () => { const manifest = getBaseManifest() manifest.headers = { 'x-custom-header': { - style: 'exists' - } + style: 'exists', + }, } expect(() => validateManifest(manifest)).not.toThrowError() @@ -197,8 +197,8 @@ describe('headers', () => { const manifest = getBaseManifest() manifest.headers = { 'x-custom-header': { - style: 'missing' - } + style: 'missing', + }, } expect(() => validateManifest(manifest)).not.toThrowError() @@ -209,8 +209,8 @@ describe('headers', () => { manifest.headers = { 'x-custom-header': { style: 'regex', - pattern: '^Bearer .+$' - } + pattern: '^Bearer .+$', + }, } expect(() => validateManifest(manifest)).not.toThrowError() @@ -220,8 +220,8 @@ describe('headers', () => { const manifest = getBaseManifest() manifest.headers = { 'x-custom-header': { - pattern: '^Bearer .+' - } + pattern: '^Bearer .+', + }, } expect(() => validateManifest(manifest)).toThrowErrorMatchingSnapshot() @@ -231,8 +231,8 @@ describe('headers', () => { const manifest = getBaseManifest() manifest.headers = { 'x-custom-header': { - style: 'invalid' - } + style: 'invalid', + }, } expect(() => validateManifest(manifest)).toThrowErrorMatchingSnapshot() @@ -242,8 +242,8 @@ describe('headers', () => { const manifest = getBaseManifest() manifest.headers = { 'x-custom-header': { - style: 'regex' - } + style: 'regex', + }, } expect(() => validateManifest(manifest)).toThrowErrorMatchingSnapshot() @@ -254,8 +254,8 @@ describe('headers', () => { manifest.headers = { 'x-custom-header': { style: 'regex', - pattern: '/^Bearer .+/' - } + pattern: '/^Bearer .+/', + }, } expect(() => validateManifest(manifest)).toThrowErrorMatchingSnapshot() @@ -266,8 +266,8 @@ describe('headers', () => { manifest.headers = { 'x-custom-header': { style: 'exists', - foo: 'bar' - } + foo: 'bar', + }, } expect(() => validateManifest(manifest)).toThrowErrorMatchingSnapshot() @@ -277,15 +277,15 @@ describe('headers', () => { const manifest = getBaseManifest() manifest.headers = { 'x-exists-header': { - style: 'exists' + style: 'exists', }, 'x-missing-header': { - style: 'missing' + style: 'missing', }, - 'authorization': { + authorization: { style: 'regex', - pattern: '^Bearer [a-zA-Z0-9]+$' - } + pattern: '^Bearer [a-zA-Z0-9]+$', + }, } expect(() => validateManifest(manifest)).not.toThrowError() From 360dafcb60e01c7b5af566a9950d291f1d8b1b44 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Eduardo=20Bou=C3=A7as?= Date: Fri, 11 Jul 2025 10:35:32 +0100 Subject: [PATCH 3/3] refactor: move headers to route definition --- .../manifest/__snapshots__/index.test.ts.snap | 66 +++++++++++++++++++ .../node/validation/manifest/index.test.ts | 22 +++---- .../node/validation/manifest/schema.ts | 62 ++++++++--------- 3 files changed, 108 insertions(+), 42 deletions(-) diff --git a/packages/edge-bundler/node/validation/manifest/__snapshots__/index.test.ts.snap b/packages/edge-bundler/node/validation/manifest/__snapshots__/index.test.ts.snap index fca5f2298f..3097575ee6 100644 --- a/packages/edge-bundler/node/validation/manifest/__snapshots__/index.test.ts.snap +++ b/packages/edge-bundler/node/validation/manifest/__snapshots__/index.test.ts.snap @@ -225,6 +225,72 @@ REQUIRED must have required property 'pattern' 12 | "generator": "@netlify/fake-plugin@1.0.0"] `; +exports[`route headers > should throw on additional property in headers 1`] = ` +[ManifestValidationError: Validation of Edge Functions manifest failed +ADDTIONAL PROPERTY must NOT have additional properties + + 15 | "x-custom-header": { + 16 | "style": "exists", +> 17 | "foo": "bar" + | ^^^^^ 😲 foo is not expected to be here! + 18 | } + 19 | } + 20 | }] +`; + +exports[`route headers > should throw on invalid pattern format 1`] = ` +[ManifestValidationError: Validation of Edge Functions manifest failed +FORMAT must match format "regexPattern" + + 15 | "x-custom-header": { + 16 | "style": "regex", +> 17 | "pattern": "/^Bearer .+/" + | ^^^^^^^^^^^^^^ 👈đŸŊ format must match format "regexPattern" + 18 | } + 19 | } + 20 | }] +`; + +exports[`route headers > should throw on invalid style value 1`] = ` +[ManifestValidationError: Validation of Edge Functions manifest failed +ENUM must be equal to one of the allowed values +(exists, missing, regex) + + 14 | "headers": { + 15 | "x-custom-header": { +> 16 | "style": "invalid" + | ^^^^^^^^^ 👈đŸŊ Unexpected value, should be equal to one of the allowed values + 17 | } + 18 | } + 19 | }] +`; + +exports[`route headers > should throw on missing style property 1`] = ` +[ManifestValidationError: Validation of Edge Functions manifest failed +REQUIRED must have required property 'style' + + 13 | "generator": "@netlify/fake-plugin@1.0.0", + 14 | "headers": { +> 15 | "x-custom-header": { + | ^ â˜šī¸ style is missing here! + 16 | "pattern": "^Bearer .+$" + 17 | } + 18 | }] +`; + +exports[`route headers > should throw when style is regex but pattern is missing 1`] = ` +[ManifestValidationError: Validation of Edge Functions manifest failed +REQUIRED must have required property 'pattern' + + 13 | "generator": "@netlify/fake-plugin@1.0.0", + 14 | "headers": { +> 15 | "x-custom-header": { + | ^ â˜šī¸ pattern is missing here! + 16 | "style": "regex" + 17 | } + 18 | }] +`; + exports[`should show multiple errors 1`] = ` [ManifestValidationError: Validation of Edge Functions manifest failed ADDTIONAL PROPERTY must NOT have additional properties diff --git a/packages/edge-bundler/node/validation/manifest/index.test.ts b/packages/edge-bundler/node/validation/manifest/index.test.ts index 3da72d7ad8..8f0b26f3ae 100644 --- a/packages/edge-bundler/node/validation/manifest/index.test.ts +++ b/packages/edge-bundler/node/validation/manifest/index.test.ts @@ -181,10 +181,10 @@ describe('import map URL', () => { }) }) -describe('headers', () => { +describe('route headers', () => { test('should accept valid headers with exists style', () => { const manifest = getBaseManifest() - manifest.headers = { + manifest.routes[0].headers = { 'x-custom-header': { style: 'exists', }, @@ -195,7 +195,7 @@ describe('headers', () => { test('should accept valid headers with missing style', () => { const manifest = getBaseManifest() - manifest.headers = { + manifest.routes[0].headers = { 'x-custom-header': { style: 'missing', }, @@ -206,7 +206,7 @@ describe('headers', () => { test('should accept valid headers with regex style and pattern', () => { const manifest = getBaseManifest() - manifest.headers = { + manifest.routes[0].headers = { 'x-custom-header': { style: 'regex', pattern: '^Bearer .+$', @@ -218,9 +218,9 @@ describe('headers', () => { test('should throw on missing style property', () => { const manifest = getBaseManifest() - manifest.headers = { + manifest.routes[0].headers = { 'x-custom-header': { - pattern: '^Bearer .+', + pattern: '^Bearer .+$', }, } @@ -229,7 +229,7 @@ describe('headers', () => { test('should throw on invalid style value', () => { const manifest = getBaseManifest() - manifest.headers = { + manifest.routes[0].headers = { 'x-custom-header': { style: 'invalid', }, @@ -240,7 +240,7 @@ describe('headers', () => { test('should throw when style is regex but pattern is missing', () => { const manifest = getBaseManifest() - manifest.headers = { + manifest.routes[0].headers = { 'x-custom-header': { style: 'regex', }, @@ -251,7 +251,7 @@ describe('headers', () => { test('should throw on invalid pattern format', () => { const manifest = getBaseManifest() - manifest.headers = { + manifest.routes[0].headers = { 'x-custom-header': { style: 'regex', pattern: '/^Bearer .+/', @@ -263,7 +263,7 @@ describe('headers', () => { test('should throw on additional property in headers', () => { const manifest = getBaseManifest() - manifest.headers = { + manifest.routes[0].headers = { 'x-custom-header': { style: 'exists', foo: 'bar', @@ -275,7 +275,7 @@ describe('headers', () => { test('should accept multiple headers with different styles', () => { const manifest = getBaseManifest() - manifest.headers = { + manifest.routes[0].headers = { 'x-exists-header': { style: 'exists', }, diff --git a/packages/edge-bundler/node/validation/manifest/schema.ts b/packages/edge-bundler/node/validation/manifest/schema.ts index 0331d8576d..7b6cc1660b 100644 --- a/packages/edge-bundler/node/validation/manifest/schema.ts +++ b/packages/edge-bundler/node/validation/manifest/schema.ts @@ -18,6 +18,36 @@ const excludedPatternsSchema = { }, } +const headersSchema = { + type: 'object', + patternProperties: { + '.*': { + type: 'object', + required: ['style'], + properties: { + pattern: { + type: 'string', + format: 'regexPattern', + }, + style: { + type: 'string', + enum: ['exists', 'missing', 'regex'], + }, + }, + additionalProperties: false, + if: { + properties: { + style: { const: 'regex' }, + }, + }, + then: { + required: ['pattern'], + }, + }, + }, + additionalProperties: false, +} + const routesSchema = { type: 'object', required: ['function', 'pattern'], @@ -36,6 +66,7 @@ const routesSchema = { type: 'array', items: { type: 'string', enum: ['GET', 'POST', 'PUT', 'PATCH', 'DELETE', 'OPTIONS'] }, }, + headers: headersSchema, }, additionalProperties: false, } @@ -60,36 +91,6 @@ const layersSchema = { additionalProperties: false, } -const headersSchema = { - type: 'object', - patternProperties: { - '.*': { - type: 'object', - required: ['style'], - properties: { - pattern: { - type: 'string', - format: 'regexPattern', - }, - style: { - type: 'string', - enum: ['exists', 'missing', 'regex'], - }, - }, - additionalProperties: false, - if: { - properties: { - style: { const: 'regex' }, - }, - }, - then: { - required: ['pattern'], - }, - }, - }, - additionalProperties: false, -} - const edgeManifestSchema = { type: 'object', required: ['bundles', 'routes', 'bundler_version'], @@ -113,7 +114,6 @@ const edgeManifestSchema = { import_map: { type: 'string' }, bundler_version: { type: 'string' }, function_config: { type: 'object', additionalProperties: functionConfigSchema }, - headers: headersSchema, }, additionalProperties: false, }