From 01ca1ba4cff709918ddc6fc113474a92c74347e9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jos=C3=A9=20Filho?= Date: Wed, 10 Jun 2026 15:44:48 -0300 Subject: [PATCH 1/9] refactor(config): reorganize schemas and normalize firewall behavior shape - move schema files from helpers/ to a dedicated schemas/ directory and extract the firewall schema into its own module (firewall/behaviors, firewall/functionsInstances). - replace the property-per-behavior pattern ({ deny: true }, { runFunction: ... }, { setWafRuleset: {...} }) with a uniform { type, attributes } shape across firewall behaviors, aligning the config contract with the API representation. --- .../converterJsonConfig/index.ts | 2 +- .../secure/firewallProcessConfigStrategy.ts | 71 ++-- .../{helpers => schemas}/behaviors.ts | 0 .../schemas/firewall/behaviors/index.ts | 191 ++++++++++ .../firewall/functionsInstances/index.ts | 27 ++ .../configProcessor/schemas/firewall/index.ts | 166 +++++++++ .../{helpers => schemas}/schema.ts | 328 +----------------- .../{helpers => schemas}/schemaManifest.ts | 0 .../configProcessor/validateConfig/index.ts | 2 +- .../configProcessor/validateManifest/index.ts | 2 +- packages/config/src/constants.ts | 2 + 11 files changed, 426 insertions(+), 365 deletions(-) rename packages/config/src/configProcessor/{helpers => schemas}/behaviors.ts (100%) create mode 100644 packages/config/src/configProcessor/schemas/firewall/behaviors/index.ts create mode 100644 packages/config/src/configProcessor/schemas/firewall/functionsInstances/index.ts create mode 100644 packages/config/src/configProcessor/schemas/firewall/index.ts rename packages/config/src/configProcessor/{helpers => schemas}/schema.ts (84%) rename packages/config/src/configProcessor/{helpers => schemas}/schemaManifest.ts (100%) diff --git a/packages/config/src/configProcessor/converterJsonConfig/index.ts b/packages/config/src/configProcessor/converterJsonConfig/index.ts index 19e64ce4..98bf7dc5 100644 --- a/packages/config/src/configProcessor/converterJsonConfig/index.ts +++ b/packages/config/src/configProcessor/converterJsonConfig/index.ts @@ -1,6 +1,6 @@ import { AzionConfig } from '../../types'; -import { schemaManifest } from '../helpers/schemaManifest'; import { factoryProcessContext } from '../processStrategy'; +import { schemaManifest } from '../schemas/schemaManifest'; import { validateConfig } from '../validateConfig'; /** diff --git a/packages/config/src/configProcessor/processStrategy/implementations/secure/firewallProcessConfigStrategy.ts b/packages/config/src/configProcessor/processStrategy/implementations/secure/firewallProcessConfigStrategy.ts index 8d61a493..b10004c4 100644 --- a/packages/config/src/configProcessor/processStrategy/implementations/secure/firewallProcessConfigStrategy.ts +++ b/packages/config/src/configProcessor/processStrategy/implementations/secure/firewallProcessConfigStrategy.ts @@ -106,10 +106,12 @@ class FirewallProcessConfigStrategy extends ProcessConfigStrategy { // and cannot be combined with other behaviors if (behaviorArray.length > 1) { const firstBehavior = behaviorArray[0]; - const hasTerminalBehavior = firstBehavior.deny || firstBehavior.drop || firstBehavior.setCustomResponse; + const hasTerminalBehavior = + firstBehavior.type === 'deny' || firstBehavior.type === 'drop' || firstBehavior.type === 'set_custom_response'; if (hasTerminalBehavior) { - const behaviorType = firstBehavior.deny ? 'deny' : firstBehavior.drop ? 'drop' : 'setCustomResponse'; + const behaviorType = + firstBehavior.type === 'deny' ? 'deny' : firstBehavior.type === 'drop' ? 'drop' : 'set_custom_response'; throw new Error( `The behavior '${behaviorType}' is a terminal behavior and must be used alone. ` + `It cannot be combined with other behaviors in the same rule.`, @@ -120,56 +122,47 @@ class FirewallProcessConfigStrategy extends ProcessConfigStrategy { const behaviors = []; for (const behaviorItem of behaviorArray) { - if (behaviorItem.runFunction !== undefined) { - behaviors.push({ - type: 'run_function', - attributes: { - value: behaviorItem.runFunction, - }, - }); + if (behaviorItem.type === 'run_function') { + behaviors.push(behaviorItem); } - if (behaviorItem.setWafRuleset) { + if (behaviorItem.type === 'set_waf_ruleset') { behaviors.push({ type: 'set_waf_ruleset', attributes: { - mode: behaviorItem.setWafRuleset.wafMode, - waf_id: behaviorItem.setWafRuleset.wafId, + mode: behaviorItem.attributes.wafMode, + waf_id: behaviorItem.attributes.wafId, }, }); } - if (behaviorItem.setRateLimit) { + if (behaviorItem.type === 'set_rate_limit') { behaviors.push({ type: 'set_rate_limit', attributes: { - type: behaviorItem.setRateLimit.type || 'second', - limit_by: behaviorItem.setRateLimit.limitBy, - average_rate_limit: behaviorItem.setRateLimit.averageRateLimit, - maximum_burst_size: behaviorItem.setRateLimit.maximumBurstSize, + type: behaviorItem.attributes.type, + limit_by: behaviorItem.attributes.limitBy, + average_rate_limit: behaviorItem.attributes.averageRateLimit, + maximum_burst_size: behaviorItem.attributes.maximumBurstSize, }, }); } - if (behaviorItem.deny) { - behaviors.push({ - type: 'deny', - }); + if (behaviorItem.type === 'deny') { + behaviors.push(behaviorItem); } - if (behaviorItem.drop) { - behaviors.push({ - type: 'drop', - }); + if (behaviorItem.type === 'drop') { + behaviors.push(behaviorItem); } - if (behaviorItem.setCustomResponse) { + if (behaviorItem.type === 'set_custom_response') { behaviors.push({ type: 'set_custom_response', attributes: { - status_code: behaviorItem.setCustomResponse.statusCode, - content_type: behaviorItem.setCustomResponse.contentType, - content_body: behaviorItem.setCustomResponse.contentBody, + status_code: behaviorItem.attributes.statusCode, + content_type: behaviorItem.attributes.contentType, + content_body: behaviorItem.attributes.contentBody, }, }); } @@ -242,20 +235,23 @@ class FirewallProcessConfigStrategy extends ProcessConfigStrategy { switch (b.type) { case 'run_function': behaviorArray.push({ - runFunction: b.attributes.value, + type: 'run_function', + attributes: b.attributes, }); break; case 'set_waf_ruleset': behaviorArray.push({ - setWafRuleset: { - wafMode: b.attributes.mode, - wafId: b.attributes.waf_id, + type: 'set_waf_ruleset', + attributes: { + mode: b.attributes.mode, + waf_id: b.attributes.waf_id, }, }); break; case 'set_rate_limit': behaviorArray.push({ - setRateLimit: { + type: 'set_rate_limit', + attributes: { type: b.attributes.type, limitBy: b.attributes.limit_by, averageRateLimit: b.attributes.average_rate_limit, @@ -264,14 +260,15 @@ class FirewallProcessConfigStrategy extends ProcessConfigStrategy { }); break; case 'deny': - behaviorArray.push({ deny: true }); + behaviorArray.push({ type: 'deny' }); break; case 'drop': - behaviorArray.push({ drop: true }); + behaviorArray.push({ type: 'drop' }); break; case 'set_custom_response': behaviorArray.push({ - setCustomResponse: { + type: 'set_custom_response', + attributes: { statusCode: b.attributes.status_code, contentType: b.attributes.content_type, contentBody: b.attributes.content_body, diff --git a/packages/config/src/configProcessor/helpers/behaviors.ts b/packages/config/src/configProcessor/schemas/behaviors.ts similarity index 100% rename from packages/config/src/configProcessor/helpers/behaviors.ts rename to packages/config/src/configProcessor/schemas/behaviors.ts diff --git a/packages/config/src/configProcessor/schemas/firewall/behaviors/index.ts b/packages/config/src/configProcessor/schemas/firewall/behaviors/index.ts new file mode 100644 index 00000000..3cad06a1 --- /dev/null +++ b/packages/config/src/configProcessor/schemas/firewall/behaviors/index.ts @@ -0,0 +1,191 @@ +import { + FIREWALL_NO_ARGS_BEHAVIORS, + FIREWALL_RATE_LIMIT_BY, + FIREWALL_RATE_LIMIT_TYPES, + FIREWALL_WAF_MODES, +} from '../../../../constants'; + +const setRateLimitBehaviorSchema = { + type: 'object', + properties: { + type: { + type: 'string', + enum: ['set_rate_limit'], + errorMessage: "The 'type' field must be a valid set_rate_limit behavior type.", + }, + attributes: { + type: 'object', + properties: { + type: { + type: 'string', + enum: FIREWALL_RATE_LIMIT_TYPES, + errorMessage: `The rate limit type must be one of: ${FIREWALL_RATE_LIMIT_TYPES.join(', ')}`, + }, + limitBy: { + type: 'string', + enum: FIREWALL_RATE_LIMIT_BY, + errorMessage: `The rate limit must be applied by one of: ${FIREWALL_RATE_LIMIT_BY.join(', ')}`, + }, + averageRateLimit: { + type: 'string', + errorMessage: 'The averageRateLimit must be a string', + }, + maximumBurstSize: { + type: 'string', + errorMessage: 'The maximumBurstSize must be a string', + }, + }, + required: ['type', 'limitBy', 'averageRateLimit', 'maximumBurstSize'], + additionalProperties: false, + errorMessage: { + additionalProperties: 'No additional properties are allowed in set_rate_limit behavior attributes.', + required: + "The 'type', 'limitBy', 'averageRateLimit', and 'maximumBurstSize' fields are required in set_rate_limit behavior attributes.", + }, + }, + }, + required: ['type', 'attributes'], + additionalProperties: false, +}; + +const setWafRuleSetBehaviorSchema = { + type: 'object', + properties: { + type: { + type: 'string', + enum: ['set_waf_rule_set'], + errorMessage: "The 'type' field must be a valid set_waf_rule_set behavior type.", + }, + attributes: { + type: 'object', + properties: { + wafMode: { + type: 'string', + enum: FIREWALL_WAF_MODES, + errorMessage: `The wafMode must be one of: ${FIREWALL_WAF_MODES.join(', ')}`, + }, + wafId: { + type: ['string', 'number'], + errorMessage: 'The wafId must be a string or number', + }, + }, + required: ['wafMode', 'wafId'], + additionalProperties: false, + errorMessage: { + additionalProperties: 'No additional properties are allowed in the setWafRuleSet object', + required: "Both 'wafMode' and 'wafId' fields are required in setWafRuleSet attributes", + }, + }, + }, + required: ['type', 'attributes'], + additionalProperties: false, +}; + +const setCustomResponseBehaviorSchema = { + type: 'object', + properties: { + type: { + type: 'string', + enum: ['set_custom_response'], + errorMessage: "The 'type' field must be a valid set_custom_response behavior type.", + }, + attributes: { + type: 'object', + properties: { + statusCode: { + type: ['integer', 'string'], + minimum: 200, + maximum: 499, + errorMessage: 'The statusCode must be a number or string between 200 and 499', + }, + contentType: { + type: 'string', + errorMessage: 'The contentType must be a string', + }, + contentBody: { + type: 'string', + errorMessage: 'The contentBody must be a string', + }, + }, + required: ['statusCode', 'contentType', 'contentBody'], + additionalProperties: false, + errorMessage: { + additionalProperties: 'No additional properties are allowed in the setCustomResponse object', + required: "All fields ('statusCode', 'contentType', 'contentBody') are required in setCustomResponse", + }, + }, + }, + required: ['type', 'attributes'], + additionalProperties: false, +}; + +const noArgsBehaviorSchema = { + type: 'object', + properties: { + type: { + type: 'string', + enum: FIREWALL_NO_ARGS_BEHAVIORS, + errorMessage: "The 'type' field must be a valid firewall no-args behavior type.", + }, + }, + required: ['type'], + additionalProperties: false, + errorMessage: { + additionalProperties: 'No additional properties are allowed in firewall no-args behaviors.', + required: "The 'type' field is required in firewall no-args behaviors.", + }, +}; + +const runFunctionBehaviorSchema = { + type: 'object', + properties: { + type: { + type: 'string', + enum: ['run_function'], + errorMessage: "The 'type' field must be 'run_function' for this behavior.", + }, + attributes: { + type: 'object', + properties: { + value: { + oneOf: [ + { + type: 'string', + minLength: 1, + maxLength: 255, + pattern: '.*', + errorMessage: "The 'value' field must be a string between 1 and 255 characters.", + }, + { + type: 'number', + minimum: 1, + errorMessage: "The 'value' field must be a positive number.", + }, + ], + errorMessage: "The 'value' field must be a string or positive number.", + }, + }, + required: ['value'], + additionalProperties: false, + errorMessage: { + additionalProperties: 'No additional properties are allowed in run_function behavior attributes.', + required: "The 'value' field is required in run_function behavior attributes.", + }, + }, + }, + required: ['type', 'attributes'], + additionalProperties: false, +}; + +const firewallBehaviorSchema = { + oneOf: [ + runFunctionBehaviorSchema, + setWafRuleSetBehaviorSchema, + setRateLimitBehaviorSchema, + noArgsBehaviorSchema, + setCustomResponseBehaviorSchema, + ], + errorMessage: 'Each behavior must match one of the valid behavior formats.', +}; + +export default firewallBehaviorSchema; diff --git a/packages/config/src/configProcessor/schemas/firewall/functionsInstances/index.ts b/packages/config/src/configProcessor/schemas/firewall/functionsInstances/index.ts new file mode 100644 index 00000000..e0c7e6a6 --- /dev/null +++ b/packages/config/src/configProcessor/schemas/firewall/functionsInstances/index.ts @@ -0,0 +1,27 @@ +const firewallFunctionsInstances = { + type: 'object', + properties: { + name: { + type: 'string', + minLength: 1, + maxLength: 100, + errorMessage: 'The name must be a string between 1 and 100 characters long', + }, + args: { + type: 'object', + default: {}, + errorMessage: "The 'args' field must be an object", + }, + ref: { + type: ['string', 'number'], + errorMessage: "The 'ref' field must be a string or number referencing an existing Function name or ID", + }, + }, + required: ['name', 'ref'], + additionalProperties: false, + errorMessage: { + additionalProperties: 'No additional properties are allowed in the firewallFunctionsInstance object', + }, +}; + +export default firewallFunctionsInstances; diff --git a/packages/config/src/configProcessor/schemas/firewall/index.ts b/packages/config/src/configProcessor/schemas/firewall/index.ts new file mode 100644 index 00000000..0375741e --- /dev/null +++ b/packages/config/src/configProcessor/schemas/firewall/index.ts @@ -0,0 +1,166 @@ +import { FIREWALL_RULE_CONDITIONAL, FIREWALL_RULE_OPERATORS, FIREWALL_VARIABLES } from '../../../constants'; +import firewallBehaviorSchema from './behaviors'; +import firewallFunctionsInstances from './functionsInstances'; + +const firewall = { + type: 'array', + items: { + type: 'object', + properties: { + name: { + type: 'string', + errorMessage: "The firewall configuration must have a 'name' field of type string", + }, + active: { + type: 'boolean', + errorMessage: "The firewall's 'active' field must be a boolean", + }, + debugRules: { + type: 'boolean', + errorMessage: "The firewall's 'debugRules' field must be a boolean", + }, + functions: { + type: 'boolean', + errorMessage: "The firewall's 'functions' field must be a boolean", + }, + networkProtection: { + type: 'boolean', + errorMessage: "The firewall's 'networkProtection' field must be a boolean", + }, + waf: { + type: 'boolean', + errorMessage: "The firewall's 'waf' field must be a boolean", + }, + rules: { + type: 'array', + items: { + type: 'object', + properties: { + name: { + type: 'string', + errorMessage: "Each firewall rule must have a 'name' field of type string", + }, + description: { + type: 'string', + errorMessage: "The rule's 'description' field must be a string", + }, + active: { + type: 'boolean', + errorMessage: "The rule's 'active' field must be a boolean", + }, + match: { + type: 'string', + errorMessage: "The rule's 'match' field must be a string containing a valid regex pattern", + }, + variable: { + anyOf: [ + { + type: 'string', + pattern: '^\\$\\{(' + [...new Set(FIREWALL_VARIABLES)].join('|') + ')\\}$', + errorMessage: "The 'variable' field must be a valid variable wrapped in ${}", + }, + { + type: 'string', + enum: [...FIREWALL_VARIABLES], + errorMessage: "The 'variable' field must be a valid firewall variable", + }, + ], + errorMessage: "The 'variable' field must be a valid variable (with or without ${})", + }, + behaviors: { + type: 'array', + items: firewallBehaviorSchema, + minItems: 1, + maxItems: 10, + errorMessage: 'The behaviors must be an array with 1-10 items.', + }, + criteria: { + type: 'array', + minItems: 1, + maxItems: 5, + items: { + type: 'object', + properties: { + conditional: { + type: 'string', + enum: FIREWALL_RULE_CONDITIONAL, + errorMessage: `The 'conditional' field must be one of: ${FIREWALL_RULE_CONDITIONAL.join(', ')}`, + }, + variable: { + anyOf: [ + { + type: 'string', + pattern: '^\\$\\{(' + [...new Set(FIREWALL_VARIABLES)].join('|') + ')\\}$', + errorMessage: "The 'variable' field must be a valid variable wrapped in ${}", + }, + { + type: 'string', + enum: [...FIREWALL_VARIABLES], + errorMessage: "The 'variable' field must be a valid firewall variable", + }, + ], + errorMessage: "The 'variable' field must be a valid variable (with or without ${})", + }, + operator: { + type: 'string', + enum: FIREWALL_RULE_OPERATORS, + errorMessage: `The 'operator' field must be one of: ${FIREWALL_RULE_OPERATORS.join(', ')}`, + }, + argument: { + type: ['string', 'number'], + errorMessage: 'The argument must be a string or number', + }, + }, + required: ['conditional', 'variable', 'operator', 'argument'], + additionalProperties: false, + errorMessage: { + additionalProperties: 'No additional properties are allowed in the criteria object', + required: + "The 'variable', 'operator', 'argument' and 'conditional' fields are required in each criteria object", + }, + }, + errorMessage: { + type: 'The criteria field must be an array with at least one criteria item', + }, + }, + }, + required: ['name', 'behaviors'], + oneOf: [ + { + required: ['match', 'variable'], + not: { required: ['criteria'] }, + errorMessage: + "When using 'match' or 'variable', both fields are required and cannot be used with 'criteria'.", + }, + { + required: ['criteria'], + not: { + anyOf: [{ required: ['match'] }, { required: ['variable'] }], + }, + errorMessage: "Cannot use 'criteria' together with 'match' or 'variable'.", + }, + ], + errorMessage: { + oneOf: "You must use either 'match' AND 'variable' together OR 'criteria', but not both at the same time", + }, + }, + }, + functionsInstances: { + type: 'array', + items: firewallFunctionsInstances, + }, + }, + required: ['name'], + additionalProperties: false, + errorMessage: { + type: 'Each firewall item must be an object', + additionalProperties: 'No additional properties are allowed in the firewall object', + required: "The 'name' field is required in each firewall object", + }, + }, + errorMessage: { + type: "The 'firewall' field must be an array of firewall objects", + }, +}; + +export default firewall; diff --git a/packages/config/src/configProcessor/helpers/schema.ts b/packages/config/src/configProcessor/schemas/schema.ts similarity index 84% rename from packages/config/src/configProcessor/helpers/schema.ts rename to packages/config/src/configProcessor/schemas/schema.ts index f27d3e57..439abc1b 100644 --- a/packages/config/src/configProcessor/helpers/schema.ts +++ b/packages/config/src/configProcessor/schemas/schema.ts @@ -11,12 +11,6 @@ import { EDGE_CONNECTOR_LOAD_BALANCE_METHOD, EDGE_CONNECTOR_TRANSPORT_POLICY, EDGE_CONNECTOR_TYPES, - FIREWALL_RATE_LIMIT_BY, - FIREWALL_RATE_LIMIT_TYPES, - FIREWALL_RULE_CONDITIONAL, - FIREWALL_RULE_OPERATORS, - FIREWALL_VARIABLES, - FIREWALL_WAF_MODES, HEADER_BEHAVIORS, ID_BEHAVIORS, NETWORK_LIST_TYPES, @@ -32,6 +26,8 @@ import { WORKLOADS_ACCESS_TYPES, } from '../../constants'; +import firewallSchema from './firewall'; + const createCriteriaBaseSchema = (isRequestPhase = false) => ({ type: 'object', properties: { @@ -486,170 +482,6 @@ const schemaKV = { }, }; -const firewallRulesBehaviorsSchema = { - type: 'array', - minItems: 1, - maxItems: 10, - items: { - type: 'object', - oneOf: [ - { - properties: { - runFunction: { - type: ['string', 'number'], - errorMessage: "The 'runFunction' behavior must be a string or number", - }, - }, - required: ['runFunction'], - additionalProperties: false, - }, - { - properties: { - setWafRuleset: { - type: 'object', - properties: { - wafMode: { - type: 'string', - enum: FIREWALL_WAF_MODES, - errorMessage: `The wafMode must be one of: ${FIREWALL_WAF_MODES.join(', ')}`, - }, - wafId: { - type: ['string', 'number'], - errorMessage: 'The wafId must be a string or number', - }, - }, - required: ['wafMode', 'wafId'], - additionalProperties: false, - errorMessage: { - additionalProperties: 'No additional properties are allowed in the setWafRuleset object', - required: "Both 'wafMode' and 'wafId' fields are required in setWafRuleset", - }, - }, - }, - required: ['setWafRuleset'], - additionalProperties: false, - }, - { - properties: { - setRateLimit: { - type: 'object', - properties: { - type: { - type: 'string', - enum: FIREWALL_RATE_LIMIT_TYPES, - errorMessage: `The rate limit type must be one of: ${FIREWALL_RATE_LIMIT_TYPES.join(', ')}`, - }, - limitBy: { - type: 'string', - enum: FIREWALL_RATE_LIMIT_BY, - errorMessage: `The rate limit must be applied by one of: ${FIREWALL_RATE_LIMIT_BY.join(', ')}`, - }, - averageRateLimit: { - type: 'string', - errorMessage: 'The averageRateLimit must be a string', - }, - maximumBurstSize: { - type: 'string', - errorMessage: 'The maximumBurstSize must be a string', - }, - }, - required: ['type', 'limitBy', 'averageRateLimit', 'maximumBurstSize'], - additionalProperties: false, - errorMessage: { - additionalProperties: 'No additional properties are allowed in the setRateLimit object', - required: - "All fields ('type', 'limitBy', 'averageRateLimit', 'maximumBurstSize') are required in setRateLimit", - }, - }, - }, - required: ['setRateLimit'], - additionalProperties: false, - }, - { - properties: { - deny: { - type: 'boolean', - const: true, - errorMessage: 'The deny behavior must be true', - }, - }, - required: ['deny'], - additionalProperties: false, - }, - { - properties: { - drop: { - type: 'boolean', - const: true, - errorMessage: 'The drop behavior must be true', - }, - }, - required: ['drop'], - additionalProperties: false, - }, - { - properties: { - setCustomResponse: { - type: 'object', - properties: { - statusCode: { - type: ['integer', 'string'], - minimum: 200, - maximum: 499, - errorMessage: 'The statusCode must be a number or string between 200 and 499', - }, - contentType: { - type: 'string', - errorMessage: 'The contentType must be a string', - }, - contentBody: { - type: 'string', - errorMessage: 'The contentBody must be a string', - }, - }, - required: ['statusCode', 'contentType', 'contentBody'], - additionalProperties: false, - errorMessage: { - additionalProperties: 'No additional properties are allowed in the setCustomResponse object', - required: "All fields ('statusCode', 'contentType', 'contentBody') are required in setCustomResponse", - }, - }, - }, - required: ['setCustomResponse'], - additionalProperties: false, - }, - ], - errorMessage: 'Each behavior item must contain exactly one behavior type', - }, - errorMessage: 'The behaviors array must contain between 1 and 10 behavior items.', -}; - -const firewallFunctionsInstances = { - type: 'object', - properties: { - name: { - type: 'string', - minLength: 1, - maxLength: 100, - errorMessage: 'The name must be a string between 1 and 100 characters long', - }, - args: { - type: 'object', - default: {}, - errorMessage: "The 'args' field must be an object", - }, - ref: { - type: ['string', 'number'], - errorMessage: "The 'ref' field must be a string or number referencing an existing Function name or ID", - }, - }, - required: ['name', 'ref'], - additionalProperties: false, - errorMessage: { - additionalProperties: 'No additional properties are allowed in the firewallFunctionsInstance object', - }, -}; - const azionConfigSchema = { $id: 'azionConfig', definitions: { @@ -1336,161 +1168,7 @@ const azionConfigSchema = { }, }, }, - firewall: { - type: 'array', - items: { - type: 'object', - properties: { - name: { - type: 'string', - errorMessage: "The firewall configuration must have a 'name' field of type string", - }, - active: { - type: 'boolean', - errorMessage: "The firewall's 'active' field must be a boolean", - }, - debugRules: { - type: 'boolean', - errorMessage: "The firewall's 'debugRules' field must be a boolean", - }, - functions: { - type: 'boolean', - errorMessage: "The firewall's 'functions' field must be a boolean", - }, - networkProtection: { - type: 'boolean', - errorMessage: "The firewall's 'networkProtection' field must be a boolean", - }, - waf: { - type: 'boolean', - errorMessage: "The firewall's 'waf' field must be a boolean", - }, - rules: { - type: 'array', - items: { - type: 'object', - properties: { - name: { - type: 'string', - errorMessage: "Each firewall rule must have a 'name' field of type string", - }, - description: { - type: 'string', - errorMessage: "The rule's 'description' field must be a string", - }, - active: { - type: 'boolean', - errorMessage: "The rule's 'active' field must be a boolean", - }, - match: { - type: 'string', - errorMessage: "The rule's 'match' field must be a string containing a valid regex pattern", - }, - variable: { - anyOf: [ - { - type: 'string', - pattern: '^\\$\\{(' + [...new Set(FIREWALL_VARIABLES)].join('|') + ')\\}$', - errorMessage: "The 'variable' field must be a valid variable wrapped in ${}", - }, - { - type: 'string', - enum: [...FIREWALL_VARIABLES], - errorMessage: "The 'variable' field must be a valid firewall variable", - }, - ], - errorMessage: "The 'variable' field must be a valid variable (with or without ${})", - }, - behaviors: firewallRulesBehaviorsSchema, - criteria: { - type: 'array', - minItems: 1, - maxItems: 5, - items: { - type: 'object', - properties: { - conditional: { - type: 'string', - enum: FIREWALL_RULE_CONDITIONAL, - errorMessage: `The 'conditional' field must be one of: ${FIREWALL_RULE_CONDITIONAL.join(', ')}`, - }, - variable: { - anyOf: [ - { - type: 'string', - pattern: '^\\$\\{(' + [...new Set(FIREWALL_VARIABLES)].join('|') + ')\\}$', - errorMessage: "The 'variable' field must be a valid variable wrapped in ${}", - }, - { - type: 'string', - enum: [...FIREWALL_VARIABLES], - errorMessage: "The 'variable' field must be a valid firewall variable", - }, - ], - errorMessage: "The 'variable' field must be a valid variable (with or without ${})", - }, - operator: { - type: 'string', - enum: FIREWALL_RULE_OPERATORS, - errorMessage: `The 'operator' field must be one of: ${FIREWALL_RULE_OPERATORS.join(', ')}`, - }, - argument: { - type: ['string', 'number'], - errorMessage: 'The argument must be a string or number', - }, - }, - required: ['conditional', 'variable', 'operator', 'argument'], - additionalProperties: false, - errorMessage: { - additionalProperties: 'No additional properties are allowed in the criteria object', - required: - "The 'variable', 'operator', 'argument' and 'conditional' fields are required in each criteria object", - }, - }, - errorMessage: { - type: 'The criteria field must be an array with at least one criteria item', - }, - }, - }, - required: ['name', 'behaviors'], - oneOf: [ - { - required: ['match', 'variable'], - not: { required: ['criteria'] }, - errorMessage: - "When using 'match' or 'variable', both fields are required and cannot be used with 'criteria'.", - }, - { - required: ['criteria'], - not: { - anyOf: [{ required: ['match'] }, { required: ['variable'] }], - }, - errorMessage: "Cannot use 'criteria' together with 'match' or 'variable'.", - }, - ], - errorMessage: { - oneOf: - "You must use either 'match' AND 'variable' together OR 'criteria', but not both at the same time", - }, - }, - }, - functionsInstances: { - type: 'array', - items: firewallFunctionsInstances, - }, - }, - required: ['name'], - additionalProperties: false, - errorMessage: { - type: 'Each firewall item must be an object', - additionalProperties: 'No additional properties are allowed in the firewall object', - required: "The 'name' field is required in each firewall object", - }, - }, - errorMessage: { - type: "The 'firewall' field must be an array of firewall objects", - }, - }, + firewall: firewallSchema, networkList: { type: 'array', items: { diff --git a/packages/config/src/configProcessor/helpers/schemaManifest.ts b/packages/config/src/configProcessor/schemas/schemaManifest.ts similarity index 100% rename from packages/config/src/configProcessor/helpers/schemaManifest.ts rename to packages/config/src/configProcessor/schemas/schemaManifest.ts diff --git a/packages/config/src/configProcessor/validateConfig/index.ts b/packages/config/src/configProcessor/validateConfig/index.ts index f9ffa15d..b44b1d2f 100644 --- a/packages/config/src/configProcessor/validateConfig/index.ts +++ b/packages/config/src/configProcessor/validateConfig/index.ts @@ -3,7 +3,7 @@ import ajvErrors from 'ajv-errors'; import addKeywords from 'ajv-keywords'; import { AzionConfig } from '../../types'; -import azionConfigSchema from '../helpers/schema'; +import azionConfigSchema from '../schemas/schema'; /** * Validates the provided configuration against a JSON Schema. diff --git a/packages/config/src/configProcessor/validateManifest/index.ts b/packages/config/src/configProcessor/validateManifest/index.ts index f15977b0..95a849de 100644 --- a/packages/config/src/configProcessor/validateManifest/index.ts +++ b/packages/config/src/configProcessor/validateManifest/index.ts @@ -2,7 +2,7 @@ import Ajv from 'ajv'; import ajvErrors from 'ajv-errors'; import addKeywords from 'ajv-keywords'; -import { schemaManifest as azionManifestSchema } from '../helpers/schemaManifest'; +import { schemaManifest as azionManifestSchema } from '../schemas/schemaManifest'; /** * Validates the provided manifest against the Azion Manifest Schema. diff --git a/packages/config/src/constants.ts b/packages/config/src/constants.ts index a0f3fecc..1054381b 100644 --- a/packages/config/src/constants.ts +++ b/packages/config/src/constants.ts @@ -337,3 +337,5 @@ export const CUSTOM_PAGE_ERROR_CODES = [ ] as const; export const CUSTOM_PAGE_TYPES = ['page_connector'] as const; + +export const FIREWALL_NO_ARGS_BEHAVIORS = ['deny', 'drop'] as const; From c7f80965e746f246f85dd8c268a54c5a025f2550 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jos=C3=A9=20Filho?= Date: Thu, 11 Jun 2026 09:47:12 -0300 Subject: [PATCH 2/9] refactor(config): split monolithic schema into per-feature modules --- .../schemas/applications/behaviors/index.ts | 217 ++ .../schemas/applications/index.ts | 462 +++++ .../configProcessor/schemas/build/index.ts | 92 + .../schemas/connectors/index.ts | 321 +++ .../schemas/customPages/index.ts | 105 + .../schemas/functions/index.ts | 76 + .../src/configProcessor/schemas/kv/index.ts | 24 + .../schemas/networkList/index.ts | 46 + .../configProcessor/schemas/purge/index.ts | 38 + .../src/configProcessor/schemas/schema.ts | 1767 +---------------- .../configProcessor/schemas/storage/index.ts | 39 + .../src/configProcessor/schemas/waf/index.ts | 114 ++ .../schemas/workloads/index.ts | 230 +++ 13 files changed, 1786 insertions(+), 1745 deletions(-) create mode 100644 packages/config/src/configProcessor/schemas/applications/behaviors/index.ts create mode 100644 packages/config/src/configProcessor/schemas/applications/index.ts create mode 100644 packages/config/src/configProcessor/schemas/build/index.ts create mode 100644 packages/config/src/configProcessor/schemas/connectors/index.ts create mode 100644 packages/config/src/configProcessor/schemas/customPages/index.ts create mode 100644 packages/config/src/configProcessor/schemas/functions/index.ts create mode 100644 packages/config/src/configProcessor/schemas/kv/index.ts create mode 100644 packages/config/src/configProcessor/schemas/networkList/index.ts create mode 100644 packages/config/src/configProcessor/schemas/purge/index.ts create mode 100644 packages/config/src/configProcessor/schemas/storage/index.ts create mode 100644 packages/config/src/configProcessor/schemas/waf/index.ts create mode 100644 packages/config/src/configProcessor/schemas/workloads/index.ts diff --git a/packages/config/src/configProcessor/schemas/applications/behaviors/index.ts b/packages/config/src/configProcessor/schemas/applications/behaviors/index.ts new file mode 100644 index 00000000..aff35ea7 --- /dev/null +++ b/packages/config/src/configProcessor/schemas/applications/behaviors/index.ts @@ -0,0 +1,217 @@ +import { + COOKIE_BEHAVIORS, + HEADER_BEHAVIORS, + ID_BEHAVIORS, + NO_ARGS_BEHAVIORS, + STRING_BEHAVIORS, +} from '../../../../constants'; + +const noArgsBehaviorSchema = { + type: 'object', + properties: { + type: { + type: 'string', + enum: NO_ARGS_BEHAVIORS, + errorMessage: "The 'type' field must be a valid no-args behavior type.", + }, + }, + required: ['type'], + additionalProperties: false, + errorMessage: { + additionalProperties: 'No additional properties are allowed in no-args behaviors.', + required: "The 'type' field is required in no-args behaviors.", + }, +}; + +const stringBehaviorSchema = { + type: 'object', + properties: { + type: { + type: 'string', + enum: STRING_BEHAVIORS, + errorMessage: "The 'type' field must be a valid string behavior type.", + }, + attributes: { + type: 'object', + properties: { + value: { + type: 'string', + minLength: 1, + maxLength: 255, + pattern: '.*', + errorMessage: "The 'value' field must be a string between 1 and 255 characters.", + }, + }, + required: ['value'], + additionalProperties: false, + errorMessage: { + additionalProperties: 'No additional properties are allowed in string behavior attributes.', + required: "The 'value' field is required in string behavior attributes.", + }, + }, + }, + required: ['type', 'attributes'], + additionalProperties: false, +}; + +// Schema para behaviors com ID value (run_function, set_cache_policy, etc.) +const idBehaviorSchema = { + type: 'object', + properties: { + type: { + type: 'string', + enum: ID_BEHAVIORS, + errorMessage: "The 'type' field must be a valid ID behavior type.", + }, + attributes: { + type: 'object', + properties: { + value: { + oneOf: [ + { + type: 'string', + minLength: 1, + maxLength: 255, + pattern: '.*', + }, + { + type: 'number', + minimum: 1, + }, + ], + errorMessage: "The 'value' field must be a string or positive number.", + }, + }, + required: ['value'], + additionalProperties: false, + errorMessage: { + additionalProperties: 'No additional properties are allowed in ID behavior attributes.', + required: "The 'value' field is required in ID behavior attributes.", + }, + }, + }, + required: ['type', 'attributes'], + additionalProperties: false, +}; + +const headerBehaviorSchema = { + type: 'object', + properties: { + type: { + type: 'string', + enum: HEADER_BEHAVIORS, + errorMessage: "The 'type' field must be a valid header behavior type.", + }, + attributes: { + type: 'object', + properties: { + value: { + type: 'string', + minLength: 1, + maxLength: 255, + pattern: '.*', + errorMessage: "The 'value' field must be a non-empty string between 1 and 255 characters.", + }, + }, + required: ['value'], + additionalProperties: false, + errorMessage: { + additionalProperties: 'No additional properties are allowed in header behavior attributes.', + required: "The 'value' field is required in header behavior attributes.", + }, + }, + }, + required: ['type', 'attributes'], + additionalProperties: false, +}; + +const cookieBehaviorSchema = { + type: 'object', + properties: { + type: { + type: 'string', + enum: COOKIE_BEHAVIORS, + errorMessage: "The 'type' field must be a valid cookie behavior type.", + }, + attributes: { + type: 'object', + properties: { + value: { + type: 'string', + minLength: 1, + maxLength: 255, + pattern: '.*', + errorMessage: "The 'value' field must be a non-empty string between 1 and 255 characters.", + }, + }, + required: ['value'], + additionalProperties: false, + errorMessage: { + additionalProperties: 'No additional properties are allowed in cookie behavior attributes.', + required: "The 'value' field is required in cookie behavior attributes.", + }, + }, + }, + required: ['type', 'attributes'], + additionalProperties: false, +}; + +const captureGroupsBehaviorSchema = { + type: 'object', + properties: { + type: { + type: 'string', + enum: ['capture_match_groups'], + errorMessage: "The 'type' field must be 'capture_match_groups'.", + }, + attributes: { + type: 'object', + properties: { + regex: { + type: 'string', + minLength: 1, + maxLength: 255, + pattern: '.*', + errorMessage: "The 'regex' field must be a string between 1 and 255 characters.", + }, + subject: { + type: 'string', + minLength: 4, + maxLength: 50, + pattern: '.*', + errorMessage: "The 'subject' field must be a string between 4 and 50 characters.", + }, + captured_array: { + type: 'string', + minLength: 1, + maxLength: 10, + pattern: '.*', + errorMessage: "The 'captured_array' field must be a string between 1 and 10 characters.", + }, + }, + required: ['regex', 'subject', 'captured_array'], + additionalProperties: false, + errorMessage: { + additionalProperties: 'No additional properties are allowed in capture groups behavior attributes.', + required: + "All fields ('regex', 'subject', 'captured_array') are required in capture groups behavior attributes.", + }, + }, + }, + required: ['type', 'attributes'], + additionalProperties: false, +}; + +const applicationRuleBehaviorSchema = { + oneOf: [ + noArgsBehaviorSchema, + stringBehaviorSchema, + idBehaviorSchema, + headerBehaviorSchema, + cookieBehaviorSchema, + captureGroupsBehaviorSchema, + ], + errorMessage: 'Each behavior must match one of the valid behavior formats.', +}; + +export default applicationRuleBehaviorSchema; diff --git a/packages/config/src/configProcessor/schemas/applications/index.ts b/packages/config/src/configProcessor/schemas/applications/index.ts new file mode 100644 index 00000000..c3499265 --- /dev/null +++ b/packages/config/src/configProcessor/schemas/applications/index.ts @@ -0,0 +1,462 @@ +import { + ALL_REQUEST_VARIABLES, + ALL_RESPONSE_VARIABLES, + RULE_CONDITIONALS, + RULE_OPERATORS_WITH_VALUE, + RULE_OPERATORS_WITHOUT_VALUE, + SPECIAL_VARIABLES, +} from '../../../constants'; +import applicationRuleBehaviorSchema from './behaviors'; + +const createVariableValidation = (isRequestPhase = false) => ({ + type: 'string', + anyOf: [ + { + type: 'string', + pattern: + '^\\$\\{(' + [...new Set(isRequestPhase ? ALL_REQUEST_VARIABLES : ALL_RESPONSE_VARIABLES)].join('|') + ')\\}$', + errorMessage: "The 'variable' field must be a valid variable wrapped in ${}", + }, + { + type: 'string', + pattern: isRequestPhase + ? '^\\$\\{(arg_|cookie_|http_)[a-zA-Z0-9_]+\\}$' + : '^\\$\\{(arg_|cookie_|http_|sent_http_|upstream_cookie_|upstream_http_)[a-zA-Z0-9_]+\\}$', + }, + { + // special variables with arguments + type: 'string', + pattern: `^\\$\\{(${SPECIAL_VARIABLES.join('|')})\\([^)]+\\)\\}$`, + }, + ], + errorMessage: isRequestPhase + ? "The 'variable' field must be a valid request phase variable wrapped in ${}, follow the patterns arg_*, cookie_*, http_*, or be a special function variable cookie_time_offset(), encode_base64()" + : "The 'variable' field must be a valid response phase variable wrapped in ${}, follow the patterns arg_*, cookie_*, http_*, sent_http_*, upstream_cookie_*, upstream_http_*, or be a special function variable cookie_time_offset(), encode_base64()", +}); + +const createCriteriaBaseSchema = (isRequestPhase = false) => ({ + type: 'object', + properties: { + variable: createVariableValidation(isRequestPhase).anyOf + ? { + type: 'string', + anyOf: createVariableValidation(isRequestPhase).anyOf, + errorMessage: createVariableValidation(isRequestPhase).errorMessage, + } + : createVariableValidation(isRequestPhase), + conditional: { + type: 'string', + enum: RULE_CONDITIONALS, + errorMessage: `The 'conditional' field must be one of: ${RULE_CONDITIONALS.join(', ')}`, + }, + operator: { + type: 'string', + enum: [...RULE_OPERATORS_WITH_VALUE, ...RULE_OPERATORS_WITHOUT_VALUE], + errorMessage: `The 'operator' field must be one of: ${[...RULE_OPERATORS_WITH_VALUE, ...RULE_OPERATORS_WITHOUT_VALUE].join(', ')}`, + }, + argument: { + type: 'string', + errorMessage: "The 'argument' field must be a string", + }, + }, + required: ['variable', 'conditional', 'operator'], + allOf: [ + { + if: { + properties: { + operator: { enum: RULE_OPERATORS_WITH_VALUE }, + }, + }, + then: { + required: ['argument'], + }, + }, + { + if: { + properties: { + operator: { enum: RULE_OPERATORS_WITHOUT_VALUE }, + }, + }, + then: { + not: { + required: ['argument'], + }, + }, + }, + ], + additionalProperties: false, +}); + +const createRuleSchema = (isRequestPhase = false) => ({ + type: 'object', + properties: { + name: { + type: 'string', + minLength: 1, + maxLength: 250, + pattern: '.*', + errorMessage: "The 'name' field must be a string between 1 and 250 characters.", + }, + description: { + type: 'string', + maxLength: 1000, + errorMessage: "The 'description' field must be a string with maximum 1000 characters.", + }, + active: { + type: 'boolean', + default: true, + errorMessage: "The 'active' field must be a boolean.", + }, + criteria: { + type: 'array', + items: { + type: 'array', + items: createCriteriaBaseSchema(isRequestPhase), + minItems: 1, + maxItems: 10, + errorMessage: 'Each criteria group must have between 1 and 10 criteria items.', + }, + minItems: 1, + maxItems: 5, + errorMessage: 'The criteria must be an array of arrays with 1-5 groups.', + }, + behaviors: { + type: 'array', + items: applicationRuleBehaviorSchema, + minItems: 1, + maxItems: 10, + errorMessage: 'The behaviors must be an array with 1-10 items.', + }, + }, + required: ['name', 'criteria', 'behaviors'], + additionalProperties: false, +}); + +const applicationsSchema = { + type: 'array', + items: { + type: 'object', + properties: { + name: { + type: 'string', + minLength: 1, + maxLength: 250, + errorMessage: "The 'name' field must be a string with 1 to 250 characters", + }, + active: { + type: 'boolean', + default: true, + errorMessage: "The 'active' field must be a boolean", + }, + debug: { + type: 'boolean', + default: false, + errorMessage: "The 'debug' field must be a boolean", + }, + edgeCacheEnabled: { + type: 'boolean', + default: true, + errorMessage: "The 'edgeCacheEnabled' field must be a boolean", + }, + functionsEnabled: { + type: 'boolean', + default: false, + errorMessage: "The 'functionsEnabled' field must be a boolean", + }, + applicationAcceleratorEnabled: { + type: 'boolean', + default: false, + errorMessage: "The 'applicationAcceleratorEnabled' field must be a boolean", + }, + imageProcessorEnabled: { + type: 'boolean', + default: false, + errorMessage: "The 'imageProcessorEnabled' field must be a boolean", + }, + cache: { + type: 'array', + items: { + type: 'object', + properties: { + name: { + type: 'string', + minLength: 1, + maxLength: 250, + errorMessage: "The 'name' field must be a string with 1 to 250 characters", + }, + stale: { + type: 'boolean', + errorMessage: "The 'stale' field must be a boolean.", + }, + queryStringSort: { + type: 'boolean', + errorMessage: "The 'queryStringSort' field must be a boolean.", + }, + tieredCache: { + type: 'object', + properties: { + enabled: { + type: 'boolean', + default: false, + errorMessage: "The 'enabled' field must be a boolean.", + }, + topology: { + type: ['string', 'null'], + enum: ['nearest-region', 'us-east-1', 'br-east-1', null], + default: 'nearest-region', + errorMessage: + "The 'topology' field must be one of 'nearest-region', 'br-east-1', 'us-east-1' or null.", + }, + }, + required: ['enabled'], + if: { + properties: { enabled: { const: true } }, + }, + then: { + required: ['enabled', 'topology'], + errorMessage: { + required: "When 'enabled' is true, 'topology' is required in the 'tiered_cache' object.", + }, + }, + additionalProperties: false, + errorMessage: { + additionalProperties: "No additional properties are allowed in the 'tiered_cache' object.", + }, + }, + methods: { + type: 'object', + properties: { + post: { + type: 'boolean', + errorMessage: "The 'post' field must be a boolean.", + }, + options: { + type: 'boolean', + errorMessage: "The 'options' field must be a boolean.", + }, + }, + additionalProperties: false, + errorMessage: { + additionalProperties: "No additional properties are allowed in the 'methods' object.", + }, + }, + browser: { + type: 'object', + properties: { + maxAgeSeconds: { + oneOf: [ + { + type: 'number', + errorMessage: "The 'maxAgeSeconds' field must be a number or a valid mathematical expression.", + }, + { + type: 'string', + pattern: '^[0-9+*/.() -]+$', + errorMessage: "The 'maxAgeSeconds' field must be a valid mathematical expression.", + }, + ], + }, + }, + required: ['maxAgeSeconds'], + additionalProperties: false, + errorMessage: { + additionalProperties: "No additional properties are allowed in the 'browser' object.", + required: "The 'maxAgeSeconds' field is required in the 'browser' object.", + }, + }, + edge: { + type: 'object', + properties: { + maxAgeSeconds: { + oneOf: [ + { + type: 'number', + errorMessage: "The 'maxAgeSeconds' field must be a number or a valid mathematical expression.", + }, + { + type: 'string', + pattern: '^[0-9+*/.() -]+$', + errorMessage: "The 'maxAgeSeconds' field must be a valid mathematical expression.", + }, + ], + }, + }, + required: ['maxAgeSeconds'], + additionalProperties: false, + errorMessage: { + additionalProperties: "No additional properties are allowed in the 'edge' object.", + required: "The 'maxAgeSeconds' field is required in the 'edge' object.", + }, + }, + cacheByCookie: { + type: 'object', + properties: { + option: { + type: 'string', + enum: ['ignore', 'all', 'allowlist', 'denylist'], + errorMessage: "The 'option' field must be one of 'ignore', 'all', 'allowlist' or 'denylist'.", + }, + list: { + type: 'array', + items: { + type: 'string', + errorMessage: "Each item in 'list' must be a string.", + }, + errorMessage: { + type: "The 'list' field must be an array of strings.", + }, + }, + }, + required: ['option'], + additionalProperties: false, + errorMessage: { + additionalProperties: "No additional properties are allowed in the 'cacheByCookie' object.", + required: "The 'option' field is required in the 'cacheByCookie' object.", + }, + if: { + properties: { + option: { enum: ['allowlist', 'denylist'] }, + }, + }, + then: { + required: ['list'], + errorMessage: { + required: "The 'list' field is required when 'option' is 'allowlist' or 'denylist'.", + }, + }, + }, + cacheByQueryString: { + type: 'object', + properties: { + option: { + type: 'string', + enum: ['ignore', 'all', 'allowlist', 'denylist'], + errorMessage: "The 'option' field must be one of 'ignore', 'all', 'allowlist' or 'denylist'.", + }, + list: { + type: 'array', + items: { + type: 'string', + errorMessage: "Each item in 'list' must be a string.", + }, + errorMessage: { + type: "The 'list' field must be an array of strings.", + }, + }, + }, + required: ['option'], + additionalProperties: false, + errorMessage: { + additionalProperties: "No additional properties are allowed in the 'cacheByQueryString' object.", + required: "The 'option' field is required in the 'cacheByQueryString' object.", + }, + if: { + properties: { + option: { enum: ['allowlist', 'denylist'] }, + }, + }, + then: { + required: ['list'], + errorMessage: { + required: "The 'list' field is required when 'option' is 'allowlist' or 'denylist'.", + }, + }, + }, + }, + required: ['name'], + additionalProperties: false, + errorMessage: { + additionalProperties: 'No additional properties are allowed in cache item objects.', + required: "The 'name' field is required in each cache item.", + }, + }, + errorMessage: "The 'cache' field must be an array of cache setting items.", + }, + rules: { + type: 'object', + properties: { + request: { + type: 'array', + items: createRuleSchema(true), + }, + response: { + type: 'array', + items: createRuleSchema(false), + }, + }, + additionalProperties: false, + }, + deviceGroups: { + type: 'array', + items: { + type: 'object', + properties: { + name: { + type: 'string', + minLength: 1, + maxLength: 250, + pattern: '.*', + errorMessage: "The 'name' field must be a string between 1 and 250 characters", + }, + userAgent: { + type: 'string', + minLength: 1, + maxLength: 512, + pattern: '.*', + errorMessage: "The 'userAgent' field must be a valid regex pattern between 1 and 512 characters", + }, + }, + required: ['name', 'userAgent'], + additionalProperties: false, + errorMessage: { + additionalProperties: 'No additional properties are allowed in device group objects', + required: "The 'name' and 'userAgent' fields are required in each device group", + }, + }, + errorMessage: "The 'deviceGroups' field must be an array of device group objects", + }, + functionsInstances: { + type: 'array', + items: { + type: 'object', + properties: { + name: { + type: 'string', + minLength: 1, + maxLength: 100, + pattern: '.*', + errorMessage: "The 'name' field must be a string between 1 and 100 characters", + }, + ref: { + type: ['string', 'number'], + errorMessage: "The 'ref' field must be a string or number referencing an existing Function name or ID", + }, + args: { + type: 'object', + default: {}, + errorMessage: "The 'args' field must be an object", + }, + active: { + type: 'boolean', + default: true, + errorMessage: "The 'active' field must be a boolean", + }, + }, + required: ['name', 'ref'], + additionalProperties: false, + errorMessage: { + additionalProperties: 'No additional properties are allowed in function instance objects', + required: "The 'name' and 'ref' fields are required in each function instance", + }, + }, + errorMessage: "The 'functions' field must be an array of function instance objects", + }, + }, + required: ['name'], + additionalProperties: false, + }, + minItems: 1, + errorMessage: "The 'applications' field must be an array of application objects with at least one item", +}; + +export default applicationsSchema; diff --git a/packages/config/src/configProcessor/schemas/build/index.ts b/packages/config/src/configProcessor/schemas/build/index.ts new file mode 100644 index 00000000..1660aae8 --- /dev/null +++ b/packages/config/src/configProcessor/schemas/build/index.ts @@ -0,0 +1,92 @@ +import { BUILD_BUNDLERS } from '../../../constants'; + +const buildSchema = { + type: 'object', + properties: { + entry: { + oneOf: [ + { type: 'string' }, + { type: 'array', items: { type: 'string' } }, + { type: 'object', additionalProperties: { type: 'string' } }, + ], + errorMessage: "The 'build.entry' must be a string, array of strings, or object with string values", + }, + bundler: { + type: 'string', + enum: BUILD_BUNDLERS, + errorMessage: "The 'build.bundler' must be either 'webpack' or 'esbuild'", + }, + preset: { + anyOf: [ + { type: 'string' }, + { + type: 'object', + properties: { + metadata: { + type: 'object', + properties: { + name: { + type: 'string', + errorMessage: "The 'name' field in preset metadata must be a string", + }, + ext: { + type: 'string', + errorMessage: "The 'ext' field in preset metadata must be a string", + }, + }, + required: ['name'], + additionalProperties: false, + errorMessage: { + additionalProperties: 'No additional properties are allowed in preset metadata', + required: "The 'name' field is required in preset metadata", + }, + }, + config: { + $ref: '#/definitions/mainConfig', + }, + handler: { instanceof: 'Function' }, + prebuild: { instanceof: 'Function' }, + postbuild: { instanceof: 'Function' }, + }, + required: ['metadata', 'config'], + additionalProperties: false, + errorMessage: { + additionalProperties: 'No additional properties are allowed in preset', + required: "Preset must contain both 'metadata' and 'config' properties", + }, + }, + ], + errorMessage: 'Preset must be either a string or an object with preset properties', + }, + polyfills: { + type: 'boolean', + errorMessage: "The 'build.polyfills' must be a boolean", + }, + worker: { + type: 'boolean', + errorMessage: "The 'build.worker' must be a boolean", + }, + extend: { + instanceof: 'Function', + errorMessage: "The 'build.extend' must be a function", + }, + memoryFS: { + type: 'object', + properties: { + injectionDirs: { + type: 'array', + items: { type: 'string' }, + }, + removePathPrefix: { type: 'string' }, + }, + required: ['injectionDirs', 'removePathPrefix'], + additionalProperties: false, + }, + }, + additionalProperties: false, + errorMessage: { + additionalProperties: "No additional properties are allowed in the 'build' object", + }, +}; + +export default buildSchema; diff --git a/packages/config/src/configProcessor/schemas/connectors/index.ts b/packages/config/src/configProcessor/schemas/connectors/index.ts new file mode 100644 index 00000000..0e1cb116 --- /dev/null +++ b/packages/config/src/configProcessor/schemas/connectors/index.ts @@ -0,0 +1,321 @@ +import { + EDGE_CONNECTOR_DNS_RESOLUTION, + EDGE_CONNECTOR_HMAC_TYPE, + EDGE_CONNECTOR_HTTP_VERSION_POLICY, + EDGE_CONNECTOR_LOAD_BALANCE_METHOD, + EDGE_CONNECTOR_TRANSPORT_POLICY, + EDGE_CONNECTOR_TYPES, +} from '../../../constants'; + +const connectorsSchema = { + type: 'array', + items: { + type: 'object', + properties: { + name: { + type: 'string', + minLength: 1, + maxLength: 255, + pattern: '.*', + errorMessage: "The 'name' field must be a string between 1 and 255 characters", + }, + active: { + type: 'boolean', + default: true, + errorMessage: "The 'active' field must be a boolean", + }, + type: { + type: 'string', + enum: EDGE_CONNECTOR_TYPES, + errorMessage: "The 'type' field must be one of: http, storage, live_ingest", + }, + attributes: { + oneOf: [ + { + type: 'object', + properties: { + bucket: { + type: 'string', + minLength: 1, + maxLength: 255, + pattern: '.*', + errorMessage: "The 'bucket' field must be a string between 1 and 255 characters", + }, + prefix: { + type: 'string', + minLength: 0, + maxLength: 255, + pattern: '.*', + errorMessage: "The 'prefix' field must be a string between 0 and 255 characters", + }, + }, + required: ['bucket', 'prefix'], + additionalProperties: false, + errorMessage: 'Storage attributes must have bucket and prefix', + }, + { + type: 'object', + properties: { + addresses: { + type: 'array', + minItems: 1, + items: { + type: 'object', + properties: { + active: { + type: 'boolean', + default: true, + errorMessage: "The 'active' field must be a boolean", + }, + address: { + type: 'string', + minLength: 1, + maxLength: 255, + pattern: '.*', + errorMessage: "The 'address' field must be a string between 1 and 255 characters", + }, + httpPort: { + type: 'integer', + minimum: 1, + maximum: 65535, + default: 80, + errorMessage: "The 'httpPort' must be between 1 and 65535", + }, + httpsPort: { + type: 'integer', + minimum: 1, + maximum: 65535, + default: 443, + errorMessage: "The 'httpsPort' must be between 1 and 65535", + }, + modules: { + type: ['object', 'null'], + errorMessage: "The 'modules' field must be an object or null", + }, + }, + required: ['address'], + additionalProperties: false, + }, + errorMessage: "The 'addresses' field must be an array of address objects", + }, + connectionOptions: { + type: 'object', + properties: { + dnsResolution: { + type: 'string', + enum: EDGE_CONNECTOR_DNS_RESOLUTION, + default: 'both', + errorMessage: "The 'dnsResolution' must be one of: both, force_ipv4", + }, + transportPolicy: { + type: 'string', + enum: EDGE_CONNECTOR_TRANSPORT_POLICY, + default: 'preserve', + errorMessage: "The 'transportPolicy' must be one of: preserve, force_https, force_http", + }, + httpVersionPolicy: { + type: 'string', + enum: EDGE_CONNECTOR_HTTP_VERSION_POLICY, + default: 'http1_1', + errorMessage: "The 'httpVersionPolicy' must be http1_1", + }, + host: { + type: 'string', + minLength: 1, + maxLength: 255, + pattern: '.*', + default: '${host}', + errorMessage: "The 'host' field must be a string between 1 and 255 characters", + }, + pathPrefix: { + type: 'string', + minLength: 0, + maxLength: 255, + pattern: '.*', + default: '', + errorMessage: "The 'pathPrefix' field must be a string between 0 and 255 characters", + }, + followingRedirect: { + type: 'boolean', + default: false, + errorMessage: "The 'followingRedirect' field must be a boolean", + }, + realIpHeader: { + type: 'string', + minLength: 1, + maxLength: 100, + pattern: '.*', + default: 'X-Real-IP', + errorMessage: "The 'realIpHeader' field must be a string between 1 and 100 characters", + }, + realPortHeader: { + type: 'string', + minLength: 1, + maxLength: 100, + pattern: '.*', + default: 'X-Real-PORT', + errorMessage: "The 'realPortHeader' field must be a string between 1 and 100 characters", + }, + }, + additionalProperties: false, + errorMessage: "The 'connectionOptions' field must be an object with valid connection options", + }, + modules: { + type: 'object', + properties: { + loadBalancer: { + type: 'object', + properties: { + enabled: { + type: 'boolean', + default: false, + errorMessage: "The 'enabled' field must be a boolean", + }, + config: { + type: ['object', 'null'], + properties: { + method: { + type: 'string', + enum: EDGE_CONNECTOR_LOAD_BALANCE_METHOD, + default: 'round_robin', + errorMessage: "The 'method' must be one of: round_robin, least_conn, ip_hash", + }, + maxRetries: { + type: 'integer', + minimum: 0, + maximum: 20, + default: 0, + errorMessage: "The 'maxRetries' must be between 0 and 20", + }, + connectionTimeout: { + type: 'integer', + minimum: 1, + maximum: 300, + default: 60, + errorMessage: "The 'connectionTimeout' must be between 1 and 300", + }, + readWriteTimeout: { + type: 'integer', + minimum: 1, + maximum: 600, + default: 120, + errorMessage: "The 'readWriteTimeout' must be between 1 and 600", + }, + }, + additionalProperties: false, + }, + }, + required: ['enabled'], + additionalProperties: false, + }, + originShield: { + type: 'object', + properties: { + enabled: { + type: 'boolean', + default: false, + errorMessage: "The 'enabled' field must be a boolean", + }, + config: { + type: ['object', 'null'], + properties: { + originIpAcl: { + type: 'object', + properties: { + enabled: { + type: 'boolean', + default: false, + errorMessage: "The 'enabled' field must be a boolean", + }, + }, + additionalProperties: false, + }, + hmac: { + type: 'object', + properties: { + enabled: { + type: 'boolean', + default: false, + errorMessage: "The 'enabled' field must be a boolean", + }, + config: { + type: ['object', 'null'], + properties: { + type: { + type: 'string', + enum: EDGE_CONNECTOR_HMAC_TYPE, + errorMessage: "The 'type' must be one of: aws4_hmac_sha256", + }, + attributes: { + type: 'object', + properties: { + region: { + type: 'string', + minLength: 1, + maxLength: 255, + pattern: '.*', + errorMessage: + "The 'region' field must be a string between 1 and 255 characters", + }, + service: { + type: 'string', + minLength: 1, + maxLength: 255, + pattern: '.*', + default: 's3', + errorMessage: + "The 'service' field must be a string between 1 and 255 characters", + }, + accessKey: { + type: 'string', + minLength: 1, + maxLength: 255, + pattern: '.*', + errorMessage: + "The 'accessKey' field must be a string between 1 and 255 characters", + }, + secretKey: { + type: 'string', + minLength: 1, + maxLength: 255, + pattern: '.*', + errorMessage: + "The 'secretKey' field must be a string between 1 and 255 characters", + }, + }, + required: ['region', 'accessKey', 'secretKey'], + additionalProperties: false, + }, + }, + additionalProperties: false, + }, + }, + additionalProperties: false, + }, + }, + additionalProperties: false, + }, + }, + required: ['enabled'], + additionalProperties: false, + }, + }, + required: ['loadBalancer', 'originShield'], + additionalProperties: false, + }, + }, + required: ['addresses', 'connectionOptions', 'modules'], + additionalProperties: false, + errorMessage: 'HTTP/Live Ingest attributes must have addresses, connectionOptions, and modules', + }, + ], + errorMessage: + "The 'attributes' field must match either storage format (bucket, prefix) or HTTP/Live Ingest format (addresses, connectionOptions, modules).", + }, + }, + required: ['name', 'type', 'attributes'], + additionalProperties: false, + }, +}; + +export default connectorsSchema; diff --git a/packages/config/src/configProcessor/schemas/customPages/index.ts b/packages/config/src/configProcessor/schemas/customPages/index.ts new file mode 100644 index 00000000..67228093 --- /dev/null +++ b/packages/config/src/configProcessor/schemas/customPages/index.ts @@ -0,0 +1,105 @@ +import { CUSTOM_PAGE_ERROR_CODES, CUSTOM_PAGE_TYPES } from '../../../constants'; + +const customPagesSchema = { + type: 'array', + items: { + type: 'object', + properties: { + name: { + type: 'string', + minLength: 1, + maxLength: 255, + pattern: '.*', + errorMessage: "The 'name' field must be a string between 1 and 255 characters", + }, + active: { + type: 'boolean', + default: true, + errorMessage: "The 'active' field must be a boolean", + }, + pages: { + type: 'array', + minItems: 1, + items: { + type: 'object', + properties: { + code: { + type: 'string', + enum: CUSTOM_PAGE_ERROR_CODES, + errorMessage: "The 'code' field must be a valid error code", + }, + page: { + type: 'object', + properties: { + type: { + type: 'string', + enum: CUSTOM_PAGE_TYPES, + default: 'page_connector', + errorMessage: "The 'type' field must be a valid page type", + }, + attributes: { + type: 'object', + properties: { + connector: { + type: ['string', 'number'], + errorMessage: + "The 'connector' field must be a string or number referencing a connector name or ID", + }, + ttl: { + type: 'integer', + minimum: 0, + maximum: 31536000, + default: 0, + errorMessage: "The 'ttl' field must be an integer between 0 and 31536000", + }, + uri: { + type: ['string', 'null'], + minLength: 1, + maxLength: 250, + pattern: '^/[/a-zA-Z0-9\\-_.~@:]*$', + errorMessage: "The 'uri' field must be a valid URI path starting with / or null", + }, + customStatusCode: { + type: ['integer', 'null'], + minimum: 100, + maximum: 599, + errorMessage: "The 'customStatusCode' field must be an integer between 100 and 599 or null", + }, + }, + required: ['connector'], + additionalProperties: false, + errorMessage: { + additionalProperties: 'No additional properties are allowed in page attributes', + required: "The 'connector' field is required in page attributes", + }, + }, + }, + required: ['attributes'], + additionalProperties: false, + errorMessage: { + additionalProperties: 'No additional properties are allowed in page configuration', + required: "The 'attributes' field is required in page configuration", + }, + }, + }, + required: ['code', 'page'], + additionalProperties: false, + errorMessage: { + additionalProperties: 'No additional properties are allowed in page entry', + required: "The 'code' and 'page' fields are required in each page entry", + }, + }, + errorMessage: "The 'pages' field must be an array of page configurations with at least one item", + }, + }, + required: ['name', 'pages'], + additionalProperties: false, + errorMessage: { + additionalProperties: 'No additional properties are allowed in custom page objects', + required: "The 'name' and 'pages' fields are required in each custom page", + }, + }, + errorMessage: "The 'customPages' field must be an array of custom page objects", +}; + +export default customPagesSchema; diff --git a/packages/config/src/configProcessor/schemas/functions/index.ts b/packages/config/src/configProcessor/schemas/functions/index.ts new file mode 100644 index 00000000..4146bec2 --- /dev/null +++ b/packages/config/src/configProcessor/schemas/functions/index.ts @@ -0,0 +1,76 @@ +const functionsSchema = { + type: 'array', + items: { + type: 'object', + properties: { + name: { + type: 'string', + minLength: 1, + maxLength: 250, + pattern: '.*', + errorMessage: "The 'name' field must be a string between 1 and 250 characters", + }, + path: { + type: 'string', + errorMessage: "The 'path' field must be a string", + }, + runtime: { + type: 'string', + enum: ['azion_js'], + default: 'azion_js', + errorMessage: "The 'runtime' field must be 'azion_js'", + }, + defaultArgs: { + type: 'object', + default: {}, + errorMessage: "The 'defaultArgs' field must be an object", + }, + executionEnvironment: { + type: 'string', + enum: ['application', 'firewall'], + default: 'application', + errorMessage: "The 'executionEnvironment' field must be 'application' or 'firewall'", + }, + active: { + type: 'boolean', + default: true, + errorMessage: "The 'active' field must be a boolean", + }, + bindings: { + type: 'object', + properties: { + storage: { + type: 'object', + properties: { + bucket: { + type: ['string', 'number'], + errorMessage: "The 'bucket' field must be a string or number", + }, + prefix: { + type: 'string', + errorMessage: "The 'prefix' field must be a string", + }, + }, + required: ['bucket', 'prefix'], + additionalProperties: false, + errorMessage: { + type: "The 'storage' field must be an object", + additionalProperties: 'No additional properties are allowed in the storage object binding.', + required: "The 'bucket' and 'prefix' fields are required in the storage object binding.", + }, + }, + }, + additionalProperties: false, + errorMessage: { + type: "The 'bindings' field must be an object", + additionalProperties: 'No additional properties are allowed in the bindings object', + }, + }, + }, + required: ['name', 'path'], + additionalProperties: false, + }, + errorMessage: "The 'functions' field must be an array of function objects with at least one item", +}; + +export default functionsSchema; diff --git a/packages/config/src/configProcessor/schemas/kv/index.ts b/packages/config/src/configProcessor/schemas/kv/index.ts new file mode 100644 index 00000000..4008898a --- /dev/null +++ b/packages/config/src/configProcessor/schemas/kv/index.ts @@ -0,0 +1,24 @@ +const kvSchema = { + type: 'array', + items: { + type: 'object', + properties: { + name: { + type: 'string', + minLength: 6, + maxLength: 63, + pattern: '^.{6,63}$', + errorMessage: "The 'name' field must be a string between 6 and 63 characters.", + }, + }, + required: ['name'], + additionalProperties: false, + errorMessage: { + additionalProperties: 'No additional properties are allowed in kv items.', + required: "The 'name' field is required.", + }, + }, + errorMessage: "The 'kv' field must be an array of kv items.", +}; + +export default kvSchema; diff --git a/packages/config/src/configProcessor/schemas/networkList/index.ts b/packages/config/src/configProcessor/schemas/networkList/index.ts new file mode 100644 index 00000000..48b36707 --- /dev/null +++ b/packages/config/src/configProcessor/schemas/networkList/index.ts @@ -0,0 +1,46 @@ +import { NETWORK_LIST_TYPES } from '../../../constants'; + +const networkListSchema = { + type: 'array', + items: { + type: 'object', + properties: { + name: { + type: 'string', + minLength: 1, + maxLength: 250, + pattern: '.*', + errorMessage: "The 'name' field must be a string between 1 and 250 characters.", + }, + type: { + type: 'string', + enum: NETWORK_LIST_TYPES, + errorMessage: "The 'type' field must be a string. Accepted values are 'ip_cidr', 'asn' or 'countries'.", + }, + items: { + type: 'array', + minItems: 1, + maxItems: 20000, + items: { + type: 'string', + pattern: '.*', + errorMessage: "Each item in 'items' must be a string.", + }, + errorMessage: "The 'items' field must be an array of strings with 1-20000 items.", + }, + active: { + type: 'boolean', + default: true, + errorMessage: "The 'active' field must be a boolean.", + }, + }, + required: ['name', 'type', 'items'], + additionalProperties: false, + errorMessage: { + additionalProperties: 'No additional properties are allowed in network list items.', + required: "The 'name', 'type' and 'items' fields are required in each network list item.", + }, + }, +}; + +export default networkListSchema; diff --git a/packages/config/src/configProcessor/schemas/purge/index.ts b/packages/config/src/configProcessor/schemas/purge/index.ts new file mode 100644 index 00000000..75596ebf --- /dev/null +++ b/packages/config/src/configProcessor/schemas/purge/index.ts @@ -0,0 +1,38 @@ +const purgeSchema = { + type: 'array', + items: { + type: 'object', + properties: { + type: { + type: 'string', + enum: ['url', 'cachekey', 'wildcard'], + errorMessage: "The 'type' field must be either 'url', 'cachekey' or 'wildcard'.", + }, + items: { + type: 'array', + minItems: 1, + items: { + type: 'string', + errorMessage: "Each item in 'items' must be a string.", + }, + errorMessage: { + type: "The 'items' field must be an array of strings.", + minItems: 'The purge items array cannot be empty. At least one item must be specified.', + }, + }, + layer: { + type: 'string', + enum: ['cache', 'tiered_cache'], + errorMessage: "The 'layer' field must be either 'cache' or 'tiered_cache'.", + }, + }, + required: ['type', 'items'], + additionalProperties: false, + errorMessage: { + additionalProperties: 'No additional properties are allowed in purge items.', + required: "The 'type and items' fields are required in each purge item.", + }, + }, +}; + +export default purgeSchema; diff --git a/packages/config/src/configProcessor/schemas/schema.ts b/packages/config/src/configProcessor/schemas/schema.ts index 439abc1b..0f4ce3f0 100644 --- a/packages/config/src/configProcessor/schemas/schema.ts +++ b/packages/config/src/configProcessor/schemas/schema.ts @@ -1,486 +1,15 @@ -import { - ALL_REQUEST_VARIABLES, - ALL_RESPONSE_VARIABLES, - BUILD_BUNDLERS, - COOKIE_BEHAVIORS, - CUSTOM_PAGE_ERROR_CODES, - CUSTOM_PAGE_TYPES, - EDGE_CONNECTOR_DNS_RESOLUTION, - EDGE_CONNECTOR_HMAC_TYPE, - EDGE_CONNECTOR_HTTP_VERSION_POLICY, - EDGE_CONNECTOR_LOAD_BALANCE_METHOD, - EDGE_CONNECTOR_TRANSPORT_POLICY, - EDGE_CONNECTOR_TYPES, - HEADER_BEHAVIORS, - ID_BEHAVIORS, - NETWORK_LIST_TYPES, - NO_ARGS_BEHAVIORS, - RULE_CONDITIONALS, - RULE_OPERATORS_WITH_VALUE, - RULE_OPERATORS_WITHOUT_VALUE, - SPECIAL_VARIABLES, - STRING_BEHAVIORS, - WORKLOAD_HTTP_VERSIONS, - WORKLOAD_MTLS_VERIFICATION, - WORKLOAD_TLS_VERSIONS, - WORKLOADS_ACCESS_TYPES, -} from '../../constants'; - +import applicationsSchema from './applications'; +import buildSchema from './build'; +import connectorsSchema from './connectors'; +import customPagesSchema from './customPages'; import firewallSchema from './firewall'; - -const createCriteriaBaseSchema = (isRequestPhase = false) => ({ - type: 'object', - properties: { - variable: createVariableValidation(isRequestPhase).anyOf - ? { - type: 'string', - anyOf: createVariableValidation(isRequestPhase).anyOf, - errorMessage: createVariableValidation(isRequestPhase).errorMessage, - } - : createVariableValidation(isRequestPhase), - conditional: { - type: 'string', - enum: RULE_CONDITIONALS, - errorMessage: `The 'conditional' field must be one of: ${RULE_CONDITIONALS.join(', ')}`, - }, - operator: { - type: 'string', - enum: [...RULE_OPERATORS_WITH_VALUE, ...RULE_OPERATORS_WITHOUT_VALUE], - errorMessage: `The 'operator' field must be one of: ${[...RULE_OPERATORS_WITH_VALUE, ...RULE_OPERATORS_WITHOUT_VALUE].join(', ')}`, - }, - argument: { - type: 'string', - errorMessage: "The 'argument' field must be a string", - }, - }, - required: ['variable', 'conditional', 'operator'], - allOf: [ - { - if: { - properties: { - operator: { enum: RULE_OPERATORS_WITH_VALUE }, - }, - }, - then: { - required: ['argument'], - }, - }, - { - if: { - properties: { - operator: { enum: RULE_OPERATORS_WITHOUT_VALUE }, - }, - }, - then: { - not: { - required: ['argument'], - }, - }, - }, - ], - additionalProperties: false, -}); - -const createVariableValidation = (isRequestPhase = false) => ({ - type: 'string', - anyOf: [ - { - type: 'string', - pattern: - '^\\$\\{(' + [...new Set(isRequestPhase ? ALL_REQUEST_VARIABLES : ALL_RESPONSE_VARIABLES)].join('|') + ')\\}$', - errorMessage: "The 'variable' field must be a valid variable wrapped in ${}", - }, - { - type: 'string', - pattern: isRequestPhase - ? '^\\$\\{(arg_|cookie_|http_)[a-zA-Z0-9_]+\\}$' - : '^\\$\\{(arg_|cookie_|http_|sent_http_|upstream_cookie_|upstream_http_)[a-zA-Z0-9_]+\\}$', - }, - { - // special variables with arguments - type: 'string', - pattern: `^\\$\\{(${SPECIAL_VARIABLES.join('|')})\\([^)]+\\)\\}$`, - }, - ], - errorMessage: isRequestPhase - ? "The 'variable' field must be a valid request phase variable wrapped in ${}, follow the patterns arg_*, cookie_*, http_*, or be a special function variable cookie_time_offset(), encode_base64()" - : "The 'variable' field must be a valid response phase variable wrapped in ${}, follow the patterns arg_*, cookie_*, http_*, sent_http_*, upstream_cookie_*, upstream_http_*, or be a special function variable cookie_time_offset(), encode_base64()", -}); - -const noArgsBehaviorSchema = { - type: 'object', - properties: { - type: { - type: 'string', - enum: NO_ARGS_BEHAVIORS, - errorMessage: "The 'type' field must be a valid no-args behavior type.", - }, - }, - required: ['type'], - additionalProperties: false, - errorMessage: { - additionalProperties: 'No additional properties are allowed in no-args behaviors.', - required: "The 'type' field is required in no-args behaviors.", - }, -}; - -const stringBehaviorSchema = { - type: 'object', - properties: { - type: { - type: 'string', - enum: STRING_BEHAVIORS, - errorMessage: "The 'type' field must be a valid string behavior type.", - }, - attributes: { - type: 'object', - properties: { - value: { - type: 'string', - minLength: 1, - maxLength: 255, - pattern: '.*', - errorMessage: "The 'value' field must be a string between 1 and 255 characters.", - }, - }, - required: ['value'], - additionalProperties: false, - errorMessage: { - additionalProperties: 'No additional properties are allowed in string behavior attributes.', - required: "The 'value' field is required in string behavior attributes.", - }, - }, - }, - required: ['type', 'attributes'], - additionalProperties: false, -}; - -// Schema para behaviors com ID value (run_function, set_cache_policy, etc.) -const idBehaviorSchema = { - type: 'object', - properties: { - type: { - type: 'string', - enum: ID_BEHAVIORS, - errorMessage: "The 'type' field must be a valid ID behavior type.", - }, - attributes: { - type: 'object', - properties: { - value: { - oneOf: [ - { - type: 'string', - minLength: 1, - maxLength: 255, - pattern: '.*', - }, - { - type: 'number', - minimum: 1, - }, - ], - errorMessage: "The 'value' field must be a string or positive number.", - }, - }, - required: ['value'], - additionalProperties: false, - errorMessage: { - additionalProperties: 'No additional properties are allowed in ID behavior attributes.', - required: "The 'value' field is required in ID behavior attributes.", - }, - }, - }, - required: ['type', 'attributes'], - additionalProperties: false, -}; - -const headerBehaviorSchema = { - type: 'object', - properties: { - type: { - type: 'string', - enum: HEADER_BEHAVIORS, - errorMessage: "The 'type' field must be a valid header behavior type.", - }, - attributes: { - type: 'object', - properties: { - value: { - type: 'string', - minLength: 1, - maxLength: 255, - pattern: '.*', - errorMessage: "The 'value' field must be a non-empty string between 1 and 255 characters.", - }, - }, - required: ['value'], - additionalProperties: false, - errorMessage: { - additionalProperties: 'No additional properties are allowed in header behavior attributes.', - required: "The 'value' field is required in header behavior attributes.", - }, - }, - }, - required: ['type', 'attributes'], - additionalProperties: false, -}; - -const cookieBehaviorSchema = { - type: 'object', - properties: { - type: { - type: 'string', - enum: COOKIE_BEHAVIORS, - errorMessage: "The 'type' field must be a valid cookie behavior type.", - }, - attributes: { - type: 'object', - properties: { - value: { - type: 'string', - minLength: 1, - maxLength: 255, - pattern: '.*', - errorMessage: "The 'value' field must be a non-empty string between 1 and 255 characters.", - }, - }, - required: ['value'], - additionalProperties: false, - errorMessage: { - additionalProperties: 'No additional properties are allowed in cookie behavior attributes.', - required: "The 'value' field is required in cookie behavior attributes.", - }, - }, - }, - required: ['type', 'attributes'], - additionalProperties: false, -}; - -const captureGroupsBehaviorSchema = { - type: 'object', - properties: { - type: { - type: 'string', - enum: ['capture_match_groups'], - errorMessage: "The 'type' field must be 'capture_match_groups'.", - }, - attributes: { - type: 'object', - properties: { - regex: { - type: 'string', - minLength: 1, - maxLength: 255, - pattern: '.*', - errorMessage: "The 'regex' field must be a string between 1 and 255 characters.", - }, - subject: { - type: 'string', - minLength: 4, - maxLength: 50, - pattern: '.*', - errorMessage: "The 'subject' field must be a string between 4 and 50 characters.", - }, - captured_array: { - type: 'string', - minLength: 1, - maxLength: 10, - pattern: '.*', - errorMessage: "The 'captured_array' field must be a string between 1 and 10 characters.", - }, - }, - required: ['regex', 'subject', 'captured_array'], - additionalProperties: false, - errorMessage: { - additionalProperties: 'No additional properties are allowed in capture groups behavior attributes.', - required: - "All fields ('regex', 'subject', 'captured_array') are required in capture groups behavior attributes.", - }, - }, - }, - required: ['type', 'attributes'], - additionalProperties: false, -}; - -const ruleBehaviorSchema = { - oneOf: [ - noArgsBehaviorSchema, - stringBehaviorSchema, - idBehaviorSchema, - headerBehaviorSchema, - cookieBehaviorSchema, - captureGroupsBehaviorSchema, - ], - errorMessage: 'Each behavior must match one of the valid behavior formats.', -}; - -const createRuleSchema = (isRequestPhase = false) => ({ - type: 'object', - properties: { - name: { - type: 'string', - minLength: 1, - maxLength: 250, - pattern: '.*', - errorMessage: "The 'name' field must be a string between 1 and 250 characters.", - }, - description: { - type: 'string', - maxLength: 1000, - errorMessage: "The 'description' field must be a string with maximum 1000 characters.", - }, - active: { - type: 'boolean', - default: true, - errorMessage: "The 'active' field must be a boolean.", - }, - criteria: { - type: 'array', - items: { - type: 'array', - items: createCriteriaBaseSchema(isRequestPhase), - minItems: 1, - maxItems: 10, - errorMessage: 'Each criteria group must have between 1 and 10 criteria items.', - }, - minItems: 1, - maxItems: 5, - errorMessage: 'The criteria must be an array of arrays with 1-5 groups.', - }, - behaviors: { - type: 'array', - items: ruleBehaviorSchema, - minItems: 1, - maxItems: 10, - errorMessage: 'The behaviors must be an array with 1-10 items.', - }, - }, - required: ['name', 'criteria', 'behaviors'], - additionalProperties: false, -}); - -const schemaFunction = { - type: 'object', - properties: { - name: { - type: 'string', - minLength: 1, - maxLength: 250, - pattern: '.*', - errorMessage: "The 'name' field must be a string between 1 and 250 characters", - }, - path: { - type: 'string', - errorMessage: "The 'path' field must be a string", - }, - runtime: { - type: 'string', - enum: ['azion_js'], - default: 'azion_js', - errorMessage: "The 'runtime' field must be 'azion_js'", - }, - defaultArgs: { - type: 'object', - default: {}, - errorMessage: "The 'defaultArgs' field must be an object", - }, - executionEnvironment: { - type: 'string', - enum: ['application', 'firewall'], - default: 'application', - errorMessage: "The 'executionEnvironment' field must be 'application' or 'firewall'", - }, - active: { - type: 'boolean', - default: true, - errorMessage: "The 'active' field must be a boolean", - }, - bindings: { - type: 'object', - properties: { - storage: { - type: 'object', - properties: { - bucket: { - type: ['string', 'number'], - errorMessage: "The 'bucket' field must be a string or number", - }, - prefix: { - type: 'string', - errorMessage: "The 'prefix' field must be a string", - }, - }, - required: ['bucket', 'prefix'], - additionalProperties: false, - errorMessage: { - type: "The 'storage' field must be an object", - additionalProperties: 'No additional properties are allowed in the storage object binding.', - required: "The 'bucket' and 'prefix' fields are required in the storage object binding.", - }, - }, - }, - additionalProperties: false, - errorMessage: { - type: "The 'bindings' field must be an object", - additionalProperties: 'No additional properties are allowed in the bindings object', - }, - }, - }, - required: ['name', 'path'], - additionalProperties: false, -}; - -const schemaStorage = { - type: 'object', - properties: { - name: { - type: 'string', - minLength: 6, - maxLength: 63, - pattern: '^.{6,63}$', - errorMessage: "The 'name' field must be a string between 6 and 63 characters.", - }, - dir: { - type: 'string', - errorMessage: "The 'dir' field must be a string.", - }, - workloadsAccess: { - type: 'string', - enum: WORKLOADS_ACCESS_TYPES, - errorMessage: "The 'workloads_access' field must be one of: read_only, read_write, restricted.", - }, - prefix: { - type: 'string', - errorMessage: "The 'prefix' field must be a string.", - }, - }, - required: ['name', 'dir', 'prefix'], - additionalProperties: false, - errorMessage: { - additionalProperties: 'No additional properties are allowed in storage items.', - required: "The 'name', 'dir' and 'prefix' fields are required.", - }, -}; - -const schemaKV = { - type: 'object', - properties: { - name: { - type: 'string', - minLength: 6, - maxLength: 63, - pattern: '^.{6,63}$', - errorMessage: "The 'name' field must be a string between 6 and 63 characters.", - }, - }, - required: ['name'], - additionalProperties: false, - errorMessage: { - additionalProperties: 'No additional properties are allowed in kv items.', - required: "The 'name' field is required.", - }, -}; +import functionsSchema from './functions'; +import kvSchema from './kv'; +import networkListSchema from './networkList'; +import purgeSchema from './purge'; +import storageSchema from './storage'; +import wafSchema from './waf'; +import workloadsSchema from './workloads'; const azionConfigSchema = { $id: 'azionConfig', @@ -488,1270 +17,18 @@ const azionConfigSchema = { mainConfig: { type: 'object', properties: { - build: { - type: 'object', - properties: { - entry: { - oneOf: [ - { type: 'string' }, - { type: 'array', items: { type: 'string' } }, - { type: 'object', additionalProperties: { type: 'string' } }, - ], - errorMessage: "The 'build.entry' must be a string, array of strings, or object with string values", - }, - bundler: { - type: 'string', - enum: BUILD_BUNDLERS, - errorMessage: "The 'build.bundler' must be either 'webpack' or 'esbuild'", - }, - preset: { - anyOf: [ - { type: 'string' }, - { - type: 'object', - properties: { - metadata: { - type: 'object', - properties: { - name: { - type: 'string', - errorMessage: "The 'name' field in preset metadata must be a string", - }, - ext: { - type: 'string', - errorMessage: "The 'ext' field in preset metadata must be a string", - }, - }, - required: ['name'], - additionalProperties: false, - errorMessage: { - additionalProperties: 'No additional properties are allowed in preset metadata', - required: "The 'name' field is required in preset metadata", - }, - }, - config: { - $ref: '#/definitions/mainConfig', - }, - handler: { instanceof: 'Function' }, - prebuild: { instanceof: 'Function' }, - postbuild: { instanceof: 'Function' }, - }, - required: ['metadata', 'config'], - additionalProperties: false, - errorMessage: { - additionalProperties: 'No additional properties are allowed in preset', - required: "Preset must contain both 'metadata' and 'config' properties", - }, - }, - ], - errorMessage: 'Preset must be either a string or an object with preset properties', - }, - polyfills: { - type: 'boolean', - errorMessage: "The 'build.polyfills' must be a boolean", - }, - worker: { - type: 'boolean', - errorMessage: "The 'build.worker' must be a boolean", - }, - extend: { - instanceof: 'Function', - errorMessage: "The 'build.extend' must be a function", - }, - memoryFS: { - type: 'object', - properties: { - injectionDirs: { - type: 'array', - items: { type: 'string' }, - }, - removePathPrefix: { type: 'string' }, - }, - required: ['injectionDirs', 'removePathPrefix'], - additionalProperties: false, - }, - }, - additionalProperties: false, - errorMessage: { - additionalProperties: "No additional properties are allowed in the 'build' object", - }, - }, - applications: { - type: 'array', - items: { - type: 'object', - properties: { - name: { - type: 'string', - minLength: 1, - maxLength: 250, - errorMessage: "The 'name' field must be a string with 1 to 250 characters", - }, - active: { - type: 'boolean', - default: true, - errorMessage: "The 'active' field must be a boolean", - }, - debug: { - type: 'boolean', - default: false, - errorMessage: "The 'debug' field must be a boolean", - }, - edgeCacheEnabled: { - type: 'boolean', - default: true, - errorMessage: "The 'edgeCacheEnabled' field must be a boolean", - }, - functionsEnabled: { - type: 'boolean', - default: false, - errorMessage: "The 'functionsEnabled' field must be a boolean", - }, - applicationAcceleratorEnabled: { - type: 'boolean', - default: false, - errorMessage: "The 'applicationAcceleratorEnabled' field must be a boolean", - }, - imageProcessorEnabled: { - type: 'boolean', - default: false, - errorMessage: "The 'imageProcessorEnabled' field must be a boolean", - }, - cache: { - type: 'array', - items: { - type: 'object', - properties: { - name: { - type: 'string', - minLength: 1, - maxLength: 250, - errorMessage: "The 'name' field must be a string with 1 to 250 characters", - }, - stale: { - type: 'boolean', - errorMessage: "The 'stale' field must be a boolean.", - }, - queryStringSort: { - type: 'boolean', - errorMessage: "The 'queryStringSort' field must be a boolean.", - }, - tieredCache: { - type: 'object', - properties: { - enabled: { - type: 'boolean', - default: false, - errorMessage: "The 'enabled' field must be a boolean.", - }, - topology: { - type: ['string', 'null'], - enum: ['nearest-region', 'us-east-1', 'br-east-1', null], - default: 'nearest-region', - errorMessage: - "The 'topology' field must be one of 'nearest-region', 'br-east-1', 'us-east-1' or null.", - }, - }, - required: ['enabled'], - if: { - properties: { enabled: { const: true } }, - }, - then: { - required: ['enabled', 'topology'], - errorMessage: { - required: "When 'enabled' is true, 'topology' is required in the 'tiered_cache' object.", - }, - }, - additionalProperties: false, - errorMessage: { - additionalProperties: "No additional properties are allowed in the 'tiered_cache' object.", - }, - }, - methods: { - type: 'object', - properties: { - post: { - type: 'boolean', - errorMessage: "The 'post' field must be a boolean.", - }, - options: { - type: 'boolean', - errorMessage: "The 'options' field must be a boolean.", - }, - }, - additionalProperties: false, - errorMessage: { - additionalProperties: "No additional properties are allowed in the 'methods' object.", - }, - }, - browser: { - type: 'object', - properties: { - maxAgeSeconds: { - oneOf: [ - { - type: 'number', - errorMessage: - "The 'maxAgeSeconds' field must be a number or a valid mathematical expression.", - }, - { - type: 'string', - pattern: '^[0-9+*/.() -]+$', - errorMessage: "The 'maxAgeSeconds' field must be a valid mathematical expression.", - }, - ], - }, - }, - required: ['maxAgeSeconds'], - additionalProperties: false, - errorMessage: { - additionalProperties: "No additional properties are allowed in the 'browser' object.", - required: "The 'maxAgeSeconds' field is required in the 'browser' object.", - }, - }, - edge: { - type: 'object', - properties: { - maxAgeSeconds: { - oneOf: [ - { - type: 'number', - errorMessage: - "The 'maxAgeSeconds' field must be a number or a valid mathematical expression.", - }, - { - type: 'string', - pattern: '^[0-9+*/.() -]+$', - errorMessage: "The 'maxAgeSeconds' field must be a valid mathematical expression.", - }, - ], - }, - }, - required: ['maxAgeSeconds'], - additionalProperties: false, - errorMessage: { - additionalProperties: "No additional properties are allowed in the 'edge' object.", - required: "The 'maxAgeSeconds' field is required in the 'edge' object.", - }, - }, - cacheByCookie: { - type: 'object', - properties: { - option: { - type: 'string', - enum: ['ignore', 'all', 'allowlist', 'denylist'], - errorMessage: "The 'option' field must be one of 'ignore', 'all', 'allowlist' or 'denylist'.", - }, - list: { - type: 'array', - items: { - type: 'string', - errorMessage: "Each item in 'list' must be a string.", - }, - errorMessage: { - type: "The 'list' field must be an array of strings.", - }, - }, - }, - required: ['option'], - additionalProperties: false, - errorMessage: { - additionalProperties: "No additional properties are allowed in the 'cacheByCookie' object.", - required: "The 'option' field is required in the 'cacheByCookie' object.", - }, - if: { - properties: { - option: { enum: ['allowlist', 'denylist'] }, - }, - }, - then: { - required: ['list'], - errorMessage: { - required: "The 'list' field is required when 'option' is 'allowlist' or 'denylist'.", - }, - }, - }, - cacheByQueryString: { - type: 'object', - properties: { - option: { - type: 'string', - enum: ['ignore', 'all', 'allowlist', 'denylist'], - errorMessage: "The 'option' field must be one of 'ignore', 'all', 'allowlist' or 'denylist'.", - }, - list: { - type: 'array', - items: { - type: 'string', - errorMessage: "Each item in 'list' must be a string.", - }, - errorMessage: { - type: "The 'list' field must be an array of strings.", - }, - }, - }, - required: ['option'], - additionalProperties: false, - errorMessage: { - additionalProperties: - "No additional properties are allowed in the 'cacheByQueryString' object.", - required: "The 'option' field is required in the 'cacheByQueryString' object.", - }, - if: { - properties: { - option: { enum: ['allowlist', 'denylist'] }, - }, - }, - then: { - required: ['list'], - errorMessage: { - required: "The 'list' field is required when 'option' is 'allowlist' or 'denylist'.", - }, - }, - }, - }, - required: ['name'], - additionalProperties: false, - errorMessage: { - additionalProperties: 'No additional properties are allowed in cache item objects.', - required: "The 'name' field is required in each cache item.", - }, - }, - errorMessage: "The 'cache' field must be an array of cache setting items.", - }, - rules: { - type: 'object', - properties: { - request: { - type: 'array', - items: createRuleSchema(true), - }, - response: { - type: 'array', - items: createRuleSchema(false), - }, - }, - additionalProperties: false, - }, - deviceGroups: { - type: 'array', - items: { - type: 'object', - properties: { - name: { - type: 'string', - minLength: 1, - maxLength: 250, - pattern: '.*', - errorMessage: "The 'name' field must be a string between 1 and 250 characters", - }, - userAgent: { - type: 'string', - minLength: 1, - maxLength: 512, - pattern: '.*', - errorMessage: "The 'userAgent' field must be a valid regex pattern between 1 and 512 characters", - }, - }, - required: ['name', 'userAgent'], - additionalProperties: false, - errorMessage: { - additionalProperties: 'No additional properties are allowed in device group objects', - required: "The 'name' and 'userAgent' fields are required in each device group", - }, - }, - errorMessage: "The 'deviceGroups' field must be an array of device group objects", - }, - functionsInstances: { - type: 'array', - items: { - type: 'object', - properties: { - name: { - type: 'string', - minLength: 1, - maxLength: 100, - pattern: '.*', - errorMessage: "The 'name' field must be a string between 1 and 100 characters", - }, - ref: { - type: ['string', 'number'], - errorMessage: - "The 'ref' field must be a string or number referencing an existing Function name or ID", - }, - args: { - type: 'object', - default: {}, - errorMessage: "The 'args' field must be an object", - }, - active: { - type: 'boolean', - default: true, - errorMessage: "The 'active' field must be a boolean", - }, - }, - required: ['name', 'ref'], - additionalProperties: false, - errorMessage: { - additionalProperties: 'No additional properties are allowed in function instance objects', - required: "The 'name' and 'ref' fields are required in each function instance", - }, - }, - errorMessage: "The 'functions' field must be an array of function instance objects", - }, - }, - required: ['name'], - additionalProperties: false, - }, - minItems: 1, - errorMessage: "The 'applications' field must be an array of application objects with at least one item", - }, - workloads: { - type: 'array', - items: { - type: 'object', - properties: { - name: { - type: 'string', - minLength: 1, - maxLength: 100, - pattern: '.*', - errorMessage: "The 'name' field must be a string between 1 and 100 characters", - }, - active: { - type: 'boolean', - default: true, - errorMessage: "The 'active' field must be a boolean", - }, - infrastructure: { - type: 'integer', - enum: [1, 2], - default: 1, - errorMessage: "The 'infrastructure' must be either 1 or 2", - }, - workloadDomainAllowAccess: { - type: 'boolean', - default: true, - errorMessage: "The 'workloadDomainAllowAccess' field must be a boolean", - }, - domains: { - type: 'array', - items: { - type: 'string', - minLength: 1, - maxLength: 250, - errorMessage: 'Each domain must be a string between 1 and 250 characters', - }, - errorMessage: "The 'domains' field must be an array of domain strings", - }, - tls: { - type: 'object', - properties: { - certificate: { - type: ['integer', 'null'], - minimum: 1, - errorMessage: "The 'certificate' must be an integer >= 1 or null", - }, - ciphers: { - type: ['integer', 'null'], - enum: [1, 2, 3, 4, 5, 6, 7, 8, null], - errorMessage: "The 'ciphers' must be an integer between 1-8 or null", - }, - minimumVersion: { - type: ['string', 'null'], - enum: [...WORKLOAD_TLS_VERSIONS, null], - default: 'tls_1_3', - errorMessage: "The 'minimumVersion' must be a valid TLS version or null", - }, - }, - default: { certificate: null, ciphers: null, minimumVersion: 'tls_1_3' }, - additionalProperties: false, - }, - protocols: { - type: 'object', - properties: { - http: { - type: 'object', - properties: { - versions: { - type: 'array', - items: { - type: 'string', - enum: WORKLOAD_HTTP_VERSIONS, - }, - default: ['http1', 'http2', 'http3'], - }, - httpPorts: { - type: 'array', - items: { type: 'integer' }, - default: [80], - }, - httpsPorts: { - type: 'array', - items: { type: 'integer' }, - default: [443], - }, - quicPorts: { - type: ['array', 'null'], - items: { type: 'integer' }, - }, - }, - required: ['versions', 'httpPorts', 'httpsPorts'], - additionalProperties: false, - }, - }, - default: { - http: { - versions: ['http1', 'http2', 'http3'], - httpPorts: [80], - httpsPorts: [443], - quicPorts: [443], - }, - }, - additionalProperties: false, - }, - mtls: { - type: 'object', - properties: { - enabled: { - type: 'boolean', - default: false, - }, - config: { - type: 'object', - properties: { - verification: { - type: 'string', - enum: WORKLOAD_MTLS_VERIFICATION, - default: 'enforce', - }, - certificate: { - type: ['integer', 'null'], - minimum: 1, - }, - crl: { - type: ['array', 'null'], - items: { type: 'integer' }, - maxItems: 100, - }, - }, - additionalProperties: false, - }, - }, - additionalProperties: false, - }, - deployments: { - type: 'array', - items: { - type: 'object', - properties: { - name: { - type: 'string', - minLength: 1, - maxLength: 254, - pattern: '.*', - errorMessage: "The 'name' field must be a string between 1 and 254 characters", - }, - current: { - type: 'boolean', - default: true, - errorMessage: "The 'current' field must be a boolean", - }, - active: { - type: 'boolean', - default: true, - errorMessage: "The 'active' field must be a boolean", - }, - strategy: { - type: 'object', - properties: { - type: { - type: 'string', - minLength: 1, - maxLength: 255, - pattern: '.*', - errorMessage: "The 'type' field must be a string between 1 and 255 characters", - }, - attributes: { - type: 'object', - properties: { - application: { - type: ['string', 'number'], - errorMessage: - "The 'application' field must be a string or number referencing an existing Application name or ID", - }, - firewall: { - type: ['string', 'number', 'null'], - errorMessage: - "The 'firewall' field must be a string or number referencing an existing Firewall name/ID or null", - }, - customPage: { - type: ['string', 'number', 'null'], - minimum: 1, - errorMessage: - "The 'customPage' field must be a string or number referencing a custom page name/ID or null", - }, - }, - required: ['application'], - additionalProperties: false, - errorMessage: { - additionalProperties: 'No additional properties are allowed in strategy attributes', - required: "The 'application' field is required in strategy attributes", - }, - }, - }, - required: ['type', 'attributes'], - additionalProperties: false, - errorMessage: { - additionalProperties: 'No additional properties are allowed in strategy', - required: "The 'type' and 'attributes' fields are required in strategy", - }, - }, - }, - required: ['name', 'strategy'], - additionalProperties: false, - errorMessage: { - additionalProperties: 'No additional properties are allowed in deployment objects', - required: "The 'name' and 'strategy' fields are required in each deployment", - }, - }, - minItems: 1, - errorMessage: "The 'deployments' field must be an array of deployment objects with at least one item.", - }, - }, - required: ['name', 'deployments'], - additionalProperties: false, - errorMessage: { - additionalProperties: 'No additional properties are allowed in workload items', - required: { - name: "The 'name' field is required in workloads", - deployments: "The 'deployments' field is required in workloads", - }, - }, - }, - minItems: 1, - errorMessage: "The 'workloads' field must be an array of workloads items with at least one item.", - }, - purge: { - type: 'array', - items: { - type: 'object', - properties: { - type: { - type: 'string', - enum: ['url', 'cachekey', 'wildcard'], - errorMessage: "The 'type' field must be either 'url', 'cachekey' or 'wildcard'.", - }, - items: { - type: 'array', - minItems: 1, - items: { - type: 'string', - errorMessage: "Each item in 'items' must be a string.", - }, - errorMessage: { - type: "The 'items' field must be an array of strings.", - minItems: 'The purge items array cannot be empty. At least one item must be specified.', - }, - }, - layer: { - type: 'string', - enum: ['cache', 'tiered_cache'], - errorMessage: "The 'layer' field must be either 'cache' or 'tiered_cache'.", - }, - }, - required: ['type', 'items'], - additionalProperties: false, - errorMessage: { - additionalProperties: 'No additional properties are allowed in purge items.', - required: "The 'type and items' fields are required in each purge item.", - }, - }, - }, + build: buildSchema, + applications: applicationsSchema, + workloads: workloadsSchema, + purge: purgeSchema, firewall: firewallSchema, - networkList: { - type: 'array', - items: { - type: 'object', - properties: { - name: { - type: 'string', - minLength: 1, - maxLength: 250, - pattern: '.*', - errorMessage: "The 'name' field must be a string between 1 and 250 characters.", - }, - type: { - type: 'string', - enum: NETWORK_LIST_TYPES, - errorMessage: "The 'type' field must be a string. Accepted values are 'ip_cidr', 'asn' or 'countries'.", - }, - items: { - type: 'array', - minItems: 1, - maxItems: 20000, - items: { - type: 'string', - pattern: '.*', - errorMessage: "Each item in 'items' must be a string.", - }, - errorMessage: "The 'items' field must be an array of strings with 1-20000 items.", - }, - active: { - type: 'boolean', - default: true, - errorMessage: "The 'active' field must be a boolean.", - }, - }, - required: ['name', 'type', 'items'], - additionalProperties: false, - errorMessage: { - additionalProperties: 'No additional properties are allowed in network list items.', - required: "The 'name', 'type' and 'items' fields are required in each network list item.", - }, - }, - }, - waf: { - type: 'array', - items: { - type: 'object', - properties: { - id: { - type: 'number', - errorMessage: "The WAF configuration must have an 'id' field of type number", - }, - name: { - type: 'string', - minLength: 1, - maxLength: 250, - pattern: '.*', - errorMessage: "The WAF configuration must have a 'name' field of type string (1-250 characters)", - }, - productVersion: { - type: ['string', 'null'], - minLength: 3, - maxLength: 50, - pattern: '\\d+\\.\\d+', - default: '1.0', - errorMessage: "The 'productVersion' field must be a string matching pattern \\d+\\.\\d+ (e.g., '1.0')", - }, - engineSettings: { - type: 'object', - properties: { - engineVersion: { - type: 'string', - enum: ['2021-Q3'], - default: '2021-Q3', - errorMessage: "The 'engineVersion' field must be '2021-Q3'", - }, - type: { - type: 'string', - enum: ['score'], - default: 'score', - errorMessage: "The 'type' field must be 'score'", - }, - attributes: { - type: 'object', - properties: { - rulesets: { - type: 'array', - items: { - type: 'integer', - enum: [1], - }, - default: [1], - errorMessage: "The 'rulesets' field must be an array containing [1]", - }, - thresholds: { - type: 'array', - items: { - type: 'object', - properties: { - threat: { - type: 'string', - enum: [ - 'cross_site_scripting', - 'directory_traversal', - 'evading_tricks', - 'file_upload', - 'identified_attack', - 'remote_file_inclusion', - 'sql_injection', - 'unwanted_access', - ], - errorMessage: "The 'threat' field must be a valid threat type", - }, - sensitivity: { - type: 'string', - enum: ['highest', 'high', 'medium', 'low', 'lowest'], - default: 'medium', - errorMessage: - "The 'sensitivity' field must be one of: highest, high, medium, low, lowest", - }, - }, - required: ['threat', 'sensitivity'], - additionalProperties: false, - }, - maxItems: 8, - errorMessage: "The 'thresholds' field must be an array of threat configurations (max 8 items)", - }, - }, - required: ['rulesets', 'thresholds'], - additionalProperties: false, - errorMessage: { - additionalProperties: 'No additional properties are allowed in the attributes object', - required: "The 'rulesets' and 'thresholds' fields are required in the attributes object", - }, - }, - }, - required: ['engineVersion', 'type', 'attributes'], - additionalProperties: false, - errorMessage: { - additionalProperties: 'No additional properties are allowed in the engineSettings object', - required: - "The 'engineVersion', 'type', and 'attributes' fields are required in the engineSettings object", - }, - }, - }, - required: ['name', 'engineSettings'], - additionalProperties: false, - errorMessage: { - type: "The 'waf' field must be an object", - additionalProperties: 'No additional properties are allowed in the WAF object', - required: "The 'name, active and mode' fields are required in the WAF object", - }, - }, - errorMessage: { - type: "The 'waf' field must be an array", - }, - }, - connectors: { - type: 'array', - items: { - type: 'object', - properties: { - name: { - type: 'string', - minLength: 1, - maxLength: 255, - pattern: '.*', - errorMessage: "The 'name' field must be a string between 1 and 255 characters", - }, - active: { - type: 'boolean', - default: true, - errorMessage: "The 'active' field must be a boolean", - }, - type: { - type: 'string', - enum: EDGE_CONNECTOR_TYPES, - errorMessage: "The 'type' field must be one of: http, storage, live_ingest", - }, - attributes: { - oneOf: [ - { - type: 'object', - properties: { - bucket: { - type: 'string', - minLength: 1, - maxLength: 255, - pattern: '.*', - errorMessage: "The 'bucket' field must be a string between 1 and 255 characters", - }, - prefix: { - type: 'string', - minLength: 0, - maxLength: 255, - pattern: '.*', - errorMessage: "The 'prefix' field must be a string between 0 and 255 characters", - }, - }, - required: ['bucket', 'prefix'], - additionalProperties: false, - errorMessage: 'Storage attributes must have bucket and prefix', - }, - { - type: 'object', - properties: { - addresses: { - type: 'array', - minItems: 1, - items: { - type: 'object', - properties: { - active: { - type: 'boolean', - default: true, - errorMessage: "The 'active' field must be a boolean", - }, - address: { - type: 'string', - minLength: 1, - maxLength: 255, - pattern: '.*', - errorMessage: "The 'address' field must be a string between 1 and 255 characters", - }, - httpPort: { - type: 'integer', - minimum: 1, - maximum: 65535, - default: 80, - errorMessage: "The 'httpPort' must be between 1 and 65535", - }, - httpsPort: { - type: 'integer', - minimum: 1, - maximum: 65535, - default: 443, - errorMessage: "The 'httpsPort' must be between 1 and 65535", - }, - modules: { - type: ['object', 'null'], - errorMessage: "The 'modules' field must be an object or null", - }, - }, - required: ['address'], - additionalProperties: false, - }, - errorMessage: "The 'addresses' field must be an array of address objects", - }, - connectionOptions: { - type: 'object', - properties: { - dnsResolution: { - type: 'string', - enum: EDGE_CONNECTOR_DNS_RESOLUTION, - default: 'both', - errorMessage: "The 'dnsResolution' must be one of: both, force_ipv4", - }, - transportPolicy: { - type: 'string', - enum: EDGE_CONNECTOR_TRANSPORT_POLICY, - default: 'preserve', - errorMessage: "The 'transportPolicy' must be one of: preserve, force_https, force_http", - }, - httpVersionPolicy: { - type: 'string', - enum: EDGE_CONNECTOR_HTTP_VERSION_POLICY, - default: 'http1_1', - errorMessage: "The 'httpVersionPolicy' must be http1_1", - }, - host: { - type: 'string', - minLength: 1, - maxLength: 255, - pattern: '.*', - default: '${host}', - errorMessage: "The 'host' field must be a string between 1 and 255 characters", - }, - pathPrefix: { - type: 'string', - minLength: 0, - maxLength: 255, - pattern: '.*', - default: '', - errorMessage: "The 'pathPrefix' field must be a string between 0 and 255 characters", - }, - followingRedirect: { - type: 'boolean', - default: false, - errorMessage: "The 'followingRedirect' field must be a boolean", - }, - realIpHeader: { - type: 'string', - minLength: 1, - maxLength: 100, - pattern: '.*', - default: 'X-Real-IP', - errorMessage: "The 'realIpHeader' field must be a string between 1 and 100 characters", - }, - realPortHeader: { - type: 'string', - minLength: 1, - maxLength: 100, - pattern: '.*', - default: 'X-Real-PORT', - errorMessage: "The 'realPortHeader' field must be a string between 1 and 100 characters", - }, - }, - additionalProperties: false, - errorMessage: "The 'connectionOptions' field must be an object with valid connection options", - }, - modules: { - type: 'object', - properties: { - loadBalancer: { - type: 'object', - properties: { - enabled: { - type: 'boolean', - default: false, - errorMessage: "The 'enabled' field must be a boolean", - }, - config: { - type: ['object', 'null'], - properties: { - method: { - type: 'string', - enum: EDGE_CONNECTOR_LOAD_BALANCE_METHOD, - default: 'round_robin', - errorMessage: "The 'method' must be one of: round_robin, least_conn, ip_hash", - }, - maxRetries: { - type: 'integer', - minimum: 0, - maximum: 20, - default: 0, - errorMessage: "The 'maxRetries' must be between 0 and 20", - }, - connectionTimeout: { - type: 'integer', - minimum: 1, - maximum: 300, - default: 60, - errorMessage: "The 'connectionTimeout' must be between 1 and 300", - }, - readWriteTimeout: { - type: 'integer', - minimum: 1, - maximum: 600, - default: 120, - errorMessage: "The 'readWriteTimeout' must be between 1 and 600", - }, - }, - additionalProperties: false, - }, - }, - required: ['enabled'], - additionalProperties: false, - }, - originShield: { - type: 'object', - properties: { - enabled: { - type: 'boolean', - default: false, - errorMessage: "The 'enabled' field must be a boolean", - }, - config: { - type: ['object', 'null'], - properties: { - originIpAcl: { - type: 'object', - properties: { - enabled: { - type: 'boolean', - default: false, - errorMessage: "The 'enabled' field must be a boolean", - }, - }, - additionalProperties: false, - }, - hmac: { - type: 'object', - properties: { - enabled: { - type: 'boolean', - default: false, - errorMessage: "The 'enabled' field must be a boolean", - }, - config: { - type: ['object', 'null'], - properties: { - type: { - type: 'string', - enum: EDGE_CONNECTOR_HMAC_TYPE, - errorMessage: "The 'type' must be one of: aws4_hmac_sha256", - }, - attributes: { - type: 'object', - properties: { - region: { - type: 'string', - minLength: 1, - maxLength: 255, - pattern: '.*', - errorMessage: - "The 'region' field must be a string between 1 and 255 characters", - }, - service: { - type: 'string', - minLength: 1, - maxLength: 255, - pattern: '.*', - default: 's3', - errorMessage: - "The 'service' field must be a string between 1 and 255 characters", - }, - accessKey: { - type: 'string', - minLength: 1, - maxLength: 255, - pattern: '.*', - errorMessage: - "The 'accessKey' field must be a string between 1 and 255 characters", - }, - secretKey: { - type: 'string', - minLength: 1, - maxLength: 255, - pattern: '.*', - errorMessage: - "The 'secretKey' field must be a string between 1 and 255 characters", - }, - }, - required: ['region', 'accessKey', 'secretKey'], - additionalProperties: false, - }, - }, - additionalProperties: false, - }, - }, - additionalProperties: false, - }, - }, - additionalProperties: false, - }, - }, - required: ['enabled'], - additionalProperties: false, - }, - }, - required: ['loadBalancer', 'originShield'], - additionalProperties: false, - }, - }, - required: ['addresses', 'connectionOptions', 'modules'], - additionalProperties: false, - errorMessage: 'HTTP/Live Ingest attributes must have addresses, connectionOptions, and modules', - }, - ], - errorMessage: - "The 'attributes' field must match either storage format (bucket, prefix) or HTTP/Live Ingest format (addresses, connectionOptions, modules).", - }, - }, - required: ['name', 'type', 'attributes'], - additionalProperties: false, - }, - }, - functions: { - type: 'array', - items: schemaFunction, - errorMessage: "The 'functions' field must be an array of function objects with at least one item", - }, - customPages: { - type: 'array', - items: { - type: 'object', - properties: { - name: { - type: 'string', - minLength: 1, - maxLength: 255, - pattern: '.*', - errorMessage: "The 'name' field must be a string between 1 and 255 characters", - }, - active: { - type: 'boolean', - default: true, - errorMessage: "The 'active' field must be a boolean", - }, - pages: { - type: 'array', - minItems: 1, - items: { - type: 'object', - properties: { - code: { - type: 'string', - enum: CUSTOM_PAGE_ERROR_CODES, - errorMessage: "The 'code' field must be a valid error code", - }, - page: { - type: 'object', - properties: { - type: { - type: 'string', - enum: CUSTOM_PAGE_TYPES, - default: 'page_connector', - errorMessage: "The 'type' field must be a valid page type", - }, - attributes: { - type: 'object', - properties: { - connector: { - type: ['string', 'number'], - errorMessage: - "The 'connector' field must be a string or number referencing a connector name or ID", - }, - ttl: { - type: 'integer', - minimum: 0, - maximum: 31536000, - default: 0, - errorMessage: "The 'ttl' field must be an integer between 0 and 31536000", - }, - uri: { - type: ['string', 'null'], - minLength: 1, - maxLength: 250, - pattern: '^/[/a-zA-Z0-9\\-_.~@:]*$', - errorMessage: "The 'uri' field must be a valid URI path starting with / or null", - }, - customStatusCode: { - type: ['integer', 'null'], - minimum: 100, - maximum: 599, - errorMessage: - "The 'customStatusCode' field must be an integer between 100 and 599 or null", - }, - }, - required: ['connector'], - additionalProperties: false, - errorMessage: { - additionalProperties: 'No additional properties are allowed in page attributes', - required: "The 'connector' field is required in page attributes", - }, - }, - }, - required: ['attributes'], - additionalProperties: false, - errorMessage: { - additionalProperties: 'No additional properties are allowed in page configuration', - required: "The 'attributes' field is required in page configuration", - }, - }, - }, - required: ['code', 'page'], - additionalProperties: false, - errorMessage: { - additionalProperties: 'No additional properties are allowed in page entry', - required: "The 'code' and 'page' fields are required in each page entry", - }, - }, - errorMessage: "The 'pages' field must be an array of page configurations with at least one item", - }, - }, - required: ['name', 'pages'], - additionalProperties: false, - errorMessage: { - additionalProperties: 'No additional properties are allowed in custom page objects', - required: "The 'name' and 'pages' fields are required in each custom page", - }, - }, - errorMessage: "The 'customPages' field must be an array of custom page objects", - }, - storage: { - type: 'array', - items: schemaStorage, - errorMessage: "The 'storage' field must be an array of storage items.", - }, - kv: { - type: 'array', - items: schemaKV, - errorMessage: "The 'kv' field must be an array of kv items.", - }, + networkList: networkListSchema, + waf: wafSchema, + connectors: connectorsSchema, + functions: functionsSchema, + customPages: customPagesSchema, + storage: storageSchema, + kv: kvSchema, }, additionalProperties: false, errorMessage: { diff --git a/packages/config/src/configProcessor/schemas/storage/index.ts b/packages/config/src/configProcessor/schemas/storage/index.ts new file mode 100644 index 00000000..26349df3 --- /dev/null +++ b/packages/config/src/configProcessor/schemas/storage/index.ts @@ -0,0 +1,39 @@ +import { WORKLOADS_ACCESS_TYPES } from '../../../constants'; + +const storageSchema = { + type: 'array', + items: { + type: 'object', + properties: { + name: { + type: 'string', + minLength: 6, + maxLength: 63, + pattern: '^.{6,63}$', + errorMessage: "The 'name' field must be a string between 6 and 63 characters.", + }, + dir: { + type: 'string', + errorMessage: "The 'dir' field must be a string.", + }, + workloadsAccess: { + type: 'string', + enum: WORKLOADS_ACCESS_TYPES, + errorMessage: "The 'workloads_access' field must be one of: read_only, read_write, restricted.", + }, + prefix: { + type: 'string', + errorMessage: "The 'prefix' field must be a string.", + }, + }, + required: ['name', 'dir', 'prefix'], + additionalProperties: false, + errorMessage: { + additionalProperties: 'No additional properties are allowed in storage items.', + required: "The 'name', 'dir' and 'prefix' fields are required.", + }, + }, + errorMessage: "The 'storage' field must be an array of storage items.", +}; + +export default storageSchema; diff --git a/packages/config/src/configProcessor/schemas/waf/index.ts b/packages/config/src/configProcessor/schemas/waf/index.ts new file mode 100644 index 00000000..79f31d2c --- /dev/null +++ b/packages/config/src/configProcessor/schemas/waf/index.ts @@ -0,0 +1,114 @@ +const wafSchema = { + type: 'array', + items: { + type: 'object', + properties: { + id: { + type: 'number', + errorMessage: "The WAF configuration must have an 'id' field of type number", + }, + name: { + type: 'string', + minLength: 1, + maxLength: 250, + pattern: '.*', + errorMessage: "The WAF configuration must have a 'name' field of type string (1-250 characters)", + }, + productVersion: { + type: ['string', 'null'], + minLength: 3, + maxLength: 50, + pattern: '\\d+\\.\\d+', + default: '1.0', + errorMessage: "The 'productVersion' field must be a string matching pattern \\d+\\.\\d+ (e.g., '1.0')", + }, + engineSettings: { + type: 'object', + properties: { + engineVersion: { + type: 'string', + enum: ['2021-Q3'], + default: '2021-Q3', + errorMessage: "The 'engineVersion' field must be '2021-Q3'", + }, + type: { + type: 'string', + enum: ['score'], + default: 'score', + errorMessage: "The 'type' field must be 'score'", + }, + attributes: { + type: 'object', + properties: { + rulesets: { + type: 'array', + items: { + type: 'integer', + enum: [1], + }, + default: [1], + errorMessage: "The 'rulesets' field must be an array containing [1]", + }, + thresholds: { + type: 'array', + items: { + type: 'object', + properties: { + threat: { + type: 'string', + enum: [ + 'cross_site_scripting', + 'directory_traversal', + 'evading_tricks', + 'file_upload', + 'identified_attack', + 'remote_file_inclusion', + 'sql_injection', + 'unwanted_access', + ], + errorMessage: "The 'threat' field must be a valid threat type", + }, + sensitivity: { + type: 'string', + enum: ['highest', 'high', 'medium', 'low', 'lowest'], + default: 'medium', + errorMessage: "The 'sensitivity' field must be one of: highest, high, medium, low, lowest", + }, + }, + required: ['threat', 'sensitivity'], + additionalProperties: false, + }, + maxItems: 8, + errorMessage: "The 'thresholds' field must be an array of threat configurations (max 8 items)", + }, + }, + required: ['rulesets', 'thresholds'], + additionalProperties: false, + errorMessage: { + additionalProperties: 'No additional properties are allowed in the attributes object', + required: "The 'rulesets' and 'thresholds' fields are required in the attributes object", + }, + }, + }, + required: ['engineVersion', 'type', 'attributes'], + additionalProperties: false, + errorMessage: { + additionalProperties: 'No additional properties are allowed in the engineSettings object', + required: "The 'engineVersion', 'type', and 'attributes' fields are required in the engineSettings object", + }, + }, + }, + required: ['name', 'engineSettings'], + additionalProperties: false, + errorMessage: { + type: "The 'waf' field must be an object", + additionalProperties: 'No additional properties are allowed in the WAF object', + required: "The 'name, active and mode' fields are required in the WAF object", + }, + }, + errorMessage: { + type: "The 'waf' field must be an array", + }, +}; + +export default wafSchema; diff --git a/packages/config/src/configProcessor/schemas/workloads/index.ts b/packages/config/src/configProcessor/schemas/workloads/index.ts new file mode 100644 index 00000000..4a9539fc --- /dev/null +++ b/packages/config/src/configProcessor/schemas/workloads/index.ts @@ -0,0 +1,230 @@ +import { WORKLOAD_HTTP_VERSIONS, WORKLOAD_MTLS_VERIFICATION, WORKLOAD_TLS_VERSIONS } from '../../../constants'; + +const workloadsSchema = { + type: 'array', + items: { + type: 'object', + properties: { + name: { + type: 'string', + minLength: 1, + maxLength: 100, + pattern: '.*', + errorMessage: "The 'name' field must be a string between 1 and 100 characters", + }, + active: { + type: 'boolean', + default: true, + errorMessage: "The 'active' field must be a boolean", + }, + infrastructure: { + type: 'integer', + enum: [1, 2], + default: 1, + errorMessage: "The 'infrastructure' must be either 1 or 2", + }, + workloadDomainAllowAccess: { + type: 'boolean', + default: true, + errorMessage: "The 'workloadDomainAllowAccess' field must be a boolean", + }, + domains: { + type: 'array', + items: { + type: 'string', + minLength: 1, + maxLength: 250, + errorMessage: 'Each domain must be a string between 1 and 250 characters', + }, + errorMessage: "The 'domains' field must be an array of domain strings", + }, + tls: { + type: 'object', + properties: { + certificate: { + type: ['integer', 'null'], + minimum: 1, + errorMessage: "The 'certificate' must be an integer >= 1 or null", + }, + ciphers: { + type: ['integer', 'null'], + enum: [1, 2, 3, 4, 5, 6, 7, 8, null], + errorMessage: "The 'ciphers' must be an integer between 1-8 or null", + }, + minimumVersion: { + type: ['string', 'null'], + enum: [...WORKLOAD_TLS_VERSIONS, null], + default: 'tls_1_3', + errorMessage: "The 'minimumVersion' must be a valid TLS version or null", + }, + }, + default: { certificate: null, ciphers: null, minimumVersion: 'tls_1_3' }, + additionalProperties: false, + }, + protocols: { + type: 'object', + properties: { + http: { + type: 'object', + properties: { + versions: { + type: 'array', + items: { + type: 'string', + enum: WORKLOAD_HTTP_VERSIONS, + }, + default: ['http1', 'http2', 'http3'], + }, + httpPorts: { + type: 'array', + items: { type: 'integer' }, + default: [80], + }, + httpsPorts: { + type: 'array', + items: { type: 'integer' }, + default: [443], + }, + quicPorts: { + type: ['array', 'null'], + items: { type: 'integer' }, + }, + }, + required: ['versions', 'httpPorts', 'httpsPorts'], + additionalProperties: false, + }, + }, + default: { + http: { + versions: ['http1', 'http2', 'http3'], + httpPorts: [80], + httpsPorts: [443], + quicPorts: [443], + }, + }, + additionalProperties: false, + }, + mtls: { + type: 'object', + properties: { + enabled: { + type: 'boolean', + default: false, + }, + config: { + type: 'object', + properties: { + verification: { + type: 'string', + enum: WORKLOAD_MTLS_VERIFICATION, + default: 'enforce', + }, + certificate: { + type: ['integer', 'null'], + minimum: 1, + }, + crl: { + type: ['array', 'null'], + items: { type: 'integer' }, + maxItems: 100, + }, + }, + additionalProperties: false, + }, + }, + additionalProperties: false, + }, + deployments: { + type: 'array', + items: { + type: 'object', + properties: { + name: { + type: 'string', + minLength: 1, + maxLength: 254, + pattern: '.*', + errorMessage: "The 'name' field must be a string between 1 and 254 characters", + }, + current: { + type: 'boolean', + default: true, + errorMessage: "The 'current' field must be a boolean", + }, + active: { + type: 'boolean', + default: true, + errorMessage: "The 'active' field must be a boolean", + }, + strategy: { + type: 'object', + properties: { + type: { + type: 'string', + minLength: 1, + maxLength: 255, + pattern: '.*', + errorMessage: "The 'type' field must be a string between 1 and 255 characters", + }, + attributes: { + type: 'object', + properties: { + application: { + type: ['string', 'number'], + errorMessage: + "The 'application' field must be a string or number referencing an existing Application name or ID", + }, + firewall: { + type: ['string', 'number', 'null'], + errorMessage: + "The 'firewall' field must be a string or number referencing an existing Firewall name/ID or null", + }, + customPage: { + type: ['string', 'number', 'null'], + minimum: 1, + errorMessage: + "The 'customPage' field must be a string or number referencing a custom page name/ID or null", + }, + }, + required: ['application'], + additionalProperties: false, + errorMessage: { + additionalProperties: 'No additional properties are allowed in strategy attributes', + required: "The 'application' field is required in strategy attributes", + }, + }, + }, + required: ['type', 'attributes'], + additionalProperties: false, + errorMessage: { + additionalProperties: 'No additional properties are allowed in strategy', + required: "The 'type' and 'attributes' fields are required in strategy", + }, + }, + }, + required: ['name', 'strategy'], + additionalProperties: false, + errorMessage: { + additionalProperties: 'No additional properties are allowed in deployment objects', + required: "The 'name' and 'strategy' fields are required in each deployment", + }, + }, + minItems: 1, + errorMessage: "The 'deployments' field must be an array of deployment objects with at least one item.", + }, + }, + required: ['name', 'deployments'], + additionalProperties: false, + errorMessage: { + additionalProperties: 'No additional properties are allowed in workload items', + required: { + name: "The 'name' field is required in workloads", + deployments: "The 'deployments' field is required in workloads", + }, + }, + }, + minItems: 1, + errorMessage: "The 'workloads' field must be an array of workloads items with at least one item.", +}; + +export default workloadsSchema; From 825f6f423dd7afd57a56845081aabd314472d2e0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jos=C3=A9=20Filho?= Date: Thu, 11 Jun 2026 09:48:56 -0300 Subject: [PATCH 3/9] refactor(config): migrate AzionFirewallBehaviorItem to discriminated union --- packages/config/src/types.ts | 13 +++++++------ 1 file changed, 7 insertions(+), 6 deletions(-) diff --git a/packages/config/src/types.ts b/packages/config/src/types.ts index d2475fe1..b0b4aa54 100644 --- a/packages/config/src/types.ts +++ b/packages/config/src/types.ts @@ -569,19 +569,20 @@ export type AzionApplication = { * Individual firewall behavior types */ export type AzionFirewallBehaviorItem = - | { runFunction: string | number } - | { setWafRuleset: { wafMode: FirewallWafMode; wafId: string | number } } + | { type: 'run_function'; attributes: { value: string | number } } + | { type: 'set_waf_ruleset'; attributes: { mode: FirewallWafMode; wafId: string | number } } | { - setRateLimit: { + type: 'set_rate_limit'; + attributes: { type: FirewallRateLimitType; limitBy: FirewallRateLimitBy; averageRateLimit: string; maximumBurstSize: string; }; } - | { deny: true } - | { drop: true } - | { setCustomResponse: { statusCode: number | string; contentType: string; contentBody: string } }; + | { type: 'deny' } + | { type: 'drop' } + | { type: 'set_custom_response'; attributes: { statusCode: number | string; contentType: string; contentBody: string } }; /** * Firewall behavior configuration for Azion. From b784b0d081c9869f81d741ccba2782012fc5a652 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jos=C3=A9=20Filho?= Date: Thu, 11 Jun 2026 09:50:29 -0300 Subject: [PATCH 4/9] test(config): update firewall strategy tests to new behavior shape --- packages/config/package.json | 4 +- .../firewallProcessConfigStrategy.test.ts | 54 ++++++++++--------- .../secure/firewallProcessConfigStrategy.ts | 4 +- packages/config/tsconfig.json | 7 ++- pnpm-lock.yaml | 4 +- 5 files changed, 40 insertions(+), 33 deletions(-) diff --git a/packages/config/package.json b/packages/config/package.json index 08830769..f788429d 100644 --- a/packages/config/package.json +++ b/packages/config/package.json @@ -42,9 +42,9 @@ "devDependencies": { "@aziontech/vite-config": "workspace:*", "@jest/globals": "^29.7.0", - "@types/jest": "^29.5.12", + "@types/jest": "^29.5.14", "@types/mock-fs": "^4.13.4", - "@types/node": "^22.13.1", + "@types/node": "^22.19.15", "@types/tmp": "^0.2.6", "esbuild": "^0.25.10", "jest": "^29.7.0", diff --git a/packages/config/src/configProcessor/processStrategy/implementations/secure/firewallProcessConfigStrategy.test.ts b/packages/config/src/configProcessor/processStrategy/implementations/secure/firewallProcessConfigStrategy.test.ts index e4a6fb4d..9e73d1a9 100644 --- a/packages/config/src/configProcessor/processStrategy/implementations/secure/firewallProcessConfigStrategy.test.ts +++ b/packages/config/src/configProcessor/processStrategy/implementations/secure/firewallProcessConfigStrategy.test.ts @@ -35,7 +35,7 @@ describe('FirewallProcessConfigStrategy', () => { description: 'desc', match: '/api/*', variable: 'request_uri', - behaviors: [{ deny: true }], + behaviors: [{ type: 'deny' }], }, ], }, @@ -95,19 +95,21 @@ describe('FirewallProcessConfigStrategy', () => { { variable: 'request_method', operator: 'exists', conditional: 'and' }, ], behaviors: [ - { runFunction: 'func-1' }, - { setWafRuleset: { wafMode: 'blocking', wafId: 123 } }, + { type: 'run_function', attributes: { value: 'func-1' } }, + { type: 'set_waf_ruleset', attributes: { mode: 'blocking', wafId: 123 } }, { - setRateLimit: { + type: 'set_rate_limit', + attributes: { type: 'minute', limitBy: 'clientIp', averageRateLimit: '60', maximumBurstSize: '20', }, }, - { drop: true }, + { type: 'drop' }, { - setCustomResponse: { + type: 'set_custom_response', + attributes: { statusCode: 429, contentType: 'application/json', contentBody: '{"err":true}', @@ -167,7 +169,7 @@ describe('FirewallProcessConfigStrategy', () => { name: 'rule-runFunction-deny', match: '/api/*', variable: 'request_uri', - behaviors: [{ runFunction: 'my-function' }, { deny: true }], + behaviors: [{ type: 'run_function', attributes: { value: 'my-function' } }, { type: 'deny' }], }, ], }, @@ -197,7 +199,7 @@ describe('FirewallProcessConfigStrategy', () => { name: 'rule-single-deny', match: '/blocked/*', variable: 'request_uri', - behaviors: [{ deny: true }], + behaviors: [{ type: 'deny' }], }, ], }, @@ -224,7 +226,7 @@ describe('FirewallProcessConfigStrategy', () => { name: 'rule-drop', match: '/drop/*', variable: 'request_uri', - behaviors: [{ drop: true }], + behaviors: [{ type: 'drop' }], }, ], }, @@ -252,9 +254,9 @@ describe('FirewallProcessConfigStrategy', () => { match: '/protected/*', variable: 'request_uri', behaviors: [ - { runFunction: 123 }, - { setWafRuleset: { wafMode: 'blocking', wafId: 456 } }, - { deny: true }, + { type: 'run_function', attributes: { value: 123 } }, + { type: 'set_waf_ruleset', attributes: { mode: 'blocking', wafId: 456 } }, + { type: 'deny' }, ], }, ], @@ -288,7 +290,7 @@ describe('FirewallProcessConfigStrategy', () => { { variable: 'host', operator: 'is_equal', conditional: 'if', argument: 'example.com' }, { variable: 'request_method', operator: 'matches', conditional: 'and', argument: '^(GET|POST)$' }, ], - behaviors: [{ deny: true }], + behaviors: [{ type: 'deny' }], }, ], }, @@ -415,19 +417,20 @@ describe('FirewallProcessConfigStrategy', () => { name: 'BlockBots', active: false, behaviors: [ - { runFunction: '/path/to/f.js' }, - { setWafRuleset: { wafMode: 'learning', wafId: 999 } }, + { type: 'run_function', attributes: { value: '/path/to/f.js' } }, + { type: 'set_waf_ruleset', attributes: { mode: 'learning', wafId: 999 } }, { - setRateLimit: { + type: 'set_rate_limit', + attributes: { type: 'second', limitBy: 'global', averageRateLimit: '10', maximumBurstSize: '5', }, }, - { deny: true }, - { drop: true }, - { setCustomResponse: { statusCode: 403, contentType: 'text/plain', contentBody: 'forbidden' } }, + { type: 'deny' }, + { type: 'drop' }, + { type: 'set_custom_response', attributes: { statusCode: 403, contentType: 'text/plain', contentBody: 'forbidden' } }, ], criteria: [ { variable: 'host', operator: 'is_equal', conditional: 'if', argument: 'bad.com' }, @@ -469,7 +472,7 @@ describe('FirewallProcessConfigStrategy', () => { expect(transformedPayload.firewall?.[0]?.rules?.[0]).toMatchObject({ name: 'RunThenDeny', active: true, - behaviors: [{ runFunction: 'my-func' }, { deny: true }], + behaviors: [{ type: 'run_function', attributes: { value: 'my-func' } }, { type: 'deny' }], }); }); @@ -497,7 +500,7 @@ describe('FirewallProcessConfigStrategy', () => { expect(transformedPayload.firewall?.[0]?.rules?.[0]).toMatchObject({ name: 'DenyOnly', active: true, - behaviors: [{ deny: true }], + behaviors: [{ type: 'deny' }], }); }); @@ -527,7 +530,7 @@ describe('FirewallProcessConfigStrategy', () => { expect(transformedPayload.firewall?.[0]?.rules?.[0]).toMatchObject({ name: 'DropOnly', active: true, - behaviors: [{ drop: true }], + behaviors: [{ type: 'drop' }], }); }); @@ -569,10 +572,11 @@ describe('FirewallProcessConfigStrategy', () => { name: 'ComplexRule', active: true, behaviors: [ - { runFunction: 999 }, - { setWafRuleset: { wafMode: 'counting', wafId: 111 } }, + { type: 'run_function', attributes: { value: 999 } }, + { type: 'set_waf_ruleset', attributes: { mode: 'counting', wafId: 111 } }, { - setCustomResponse: { + type: 'set_custom_response', + attributes: { statusCode: 429, contentType: 'application/json', contentBody: '{"error":"rate limit"}', diff --git a/packages/config/src/configProcessor/processStrategy/implementations/secure/firewallProcessConfigStrategy.ts b/packages/config/src/configProcessor/processStrategy/implementations/secure/firewallProcessConfigStrategy.ts index b10004c4..757067a6 100644 --- a/packages/config/src/configProcessor/processStrategy/implementations/secure/firewallProcessConfigStrategy.ts +++ b/packages/config/src/configProcessor/processStrategy/implementations/secure/firewallProcessConfigStrategy.ts @@ -130,7 +130,7 @@ class FirewallProcessConfigStrategy extends ProcessConfigStrategy { behaviors.push({ type: 'set_waf_ruleset', attributes: { - mode: behaviorItem.attributes.wafMode, + mode: behaviorItem.attributes.mode, waf_id: behaviorItem.attributes.wafId, }, }); @@ -244,7 +244,7 @@ class FirewallProcessConfigStrategy extends ProcessConfigStrategy { type: 'set_waf_ruleset', attributes: { mode: b.attributes.mode, - waf_id: b.attributes.waf_id, + wafId: b.attributes.waf_id, }, }); break; diff --git a/packages/config/tsconfig.json b/packages/config/tsconfig.json index 301f1523..6cee4e76 100644 --- a/packages/config/tsconfig.json +++ b/packages/config/tsconfig.json @@ -10,6 +10,9 @@ "skipLibCheck": true, "moduleResolution": "bundler", "resolveJsonModule": true, - "isolatedModules": true - } + "isolatedModules": true, + "types": ["node", "jest"] + }, + "include": ["src/**/*"], + "exclude": ["node_modules"] } diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 016cdecd..32323bbe 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -306,13 +306,13 @@ importers: specifier: ^29.7.0 version: 29.7.0 '@types/jest': - specifier: ^29.5.12 + specifier: ^29.5.14 version: 29.5.14 '@types/mock-fs': specifier: ^4.13.4 version: 4.13.4 '@types/node': - specifier: ^22.13.1 + specifier: ^22.19.15 version: 22.19.15 '@types/tmp': specifier: ^0.2.6 From 4fbfc7316799e044a2949a75333fedd94a6b6955 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jos=C3=A9=20Filho?= Date: Thu, 11 Jun 2026 11:29:34 -0300 Subject: [PATCH 5/9] fix(config): align firewall behavior examples and schema with discriminated union shape --- .../configProcessor/helpers/azion.config.example.ts | 13 ++++++++++--- .../schemas/firewall/behaviors/index.ts | 8 ++++---- 2 files changed, 14 insertions(+), 7 deletions(-) diff --git a/packages/config/src/configProcessor/helpers/azion.config.example.ts b/packages/config/src/configProcessor/helpers/azion.config.example.ts index 4fef5fbb..85b17cf6 100644 --- a/packages/config/src/configProcessor/helpers/azion.config.example.ts +++ b/packages/config/src/configProcessor/helpers/azion.config.example.ts @@ -508,7 +508,7 @@ const config: AzionConfig = { ], behaviors: [ { - deny: true, + type: 'deny', }, ], }, @@ -525,10 +525,17 @@ const config: AzionConfig = { ], behaviors: [ { - runFunction: 'my_func_instance', + type: 'set_rate_limit', + attributes: { + type: 'second', + averageRateLimit: '1', + limitBy: 'clientIp', + maximumBurstSize: '1', + }, }, { - setCustomResponse: { + type: 'set_custom_response', + attributes: { statusCode: 403, contentType: 'application/json', contentBody: '{"error": "Custom error response"}', diff --git a/packages/config/src/configProcessor/schemas/firewall/behaviors/index.ts b/packages/config/src/configProcessor/schemas/firewall/behaviors/index.ts index 3cad06a1..b2c73485 100644 --- a/packages/config/src/configProcessor/schemas/firewall/behaviors/index.ts +++ b/packages/config/src/configProcessor/schemas/firewall/behaviors/index.ts @@ -53,8 +53,8 @@ const setWafRuleSetBehaviorSchema = { properties: { type: { type: 'string', - enum: ['set_waf_rule_set'], - errorMessage: "The 'type' field must be a valid set_waf_rule_set behavior type.", + enum: ['set_waf_ruleset'], + errorMessage: "The 'type' field must be a valid set_waf_ruleset behavior type.", }, attributes: { type: 'object', @@ -72,8 +72,8 @@ const setWafRuleSetBehaviorSchema = { required: ['wafMode', 'wafId'], additionalProperties: false, errorMessage: { - additionalProperties: 'No additional properties are allowed in the setWafRuleSet object', - required: "Both 'wafMode' and 'wafId' fields are required in setWafRuleSet attributes", + additionalProperties: 'No additional properties are allowed in the set_waf_ruleset object', + required: "Both 'wafMode' and 'wafId' fields are required in set_waf_ruleset attributes", }, }, }, From b38ab3830ffc0e6f579ed9393558cf7957ea3c30 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jos=C3=A9=20Filho?= Date: Thu, 11 Jun 2026 12:29:19 -0300 Subject: [PATCH 6/9] refactor(config): rename set_waf_ruleset behavior to set_waf and update docs - rename the WAF behavior type from `set_waf_ruleset` to `set_waf` across schema, strategy, constants, types, tests, and README. Also rename the `wafMode` attribute to `mode` in the set_waf schema to align with the API field naming convention. --- packages/config/README.md | 45 +++++++++---------- .../firewallProcessConfigStrategy.test.ts | 21 +++++---- .../secure/firewallProcessConfigStrategy.ts | 8 ++-- .../schemas/firewall/behaviors/index.ts | 14 +++--- .../configProcessor/schemas/schemaManifest.ts | 8 ++-- packages/config/src/constants.ts | 2 +- packages/config/src/types.ts | 9 ++-- 7 files changed, 56 insertions(+), 51 deletions(-) diff --git a/packages/config/README.md b/packages/config/README.md index 0c282ebd..e712bf98 100644 --- a/packages/config/README.md +++ b/packages/config/README.md @@ -296,14 +296,18 @@ const config = defineConfig({ name: 'rateLimit_Then_Drop', active: true, match: '^/api/sensitive/', - behavior: { - setRateLimit: { - type: 'second', - limitBy: 'clientIp', - averageRateLimit: '10', - maximumBurstSize: '20', + behaviors: [ + { + type: 'set_rate_limit', + attributes: { + type: 'second', + limitBy: 'clientIp', + averageRateLimit: '10', + maximumBurstSize: '20', + }, }, - }, + { type: 'drop' }, + ], }, ], }, @@ -833,27 +837,22 @@ Type definition for firewall rules. - `description?: string` - Description of the rule. - `active?: boolean` - Whether the rule is active. - `match?: string` - Match criteria for the rule (regex pattern). -- `behavior: AzionFirewallBehavior` - Behavior to be applied when the rule matches. +- `behaviors: AzionFirewallBehavior` - Array of behaviors to be applied when the rule matches. ### `AzionFirewallBehavior` -Type definition for firewall rule behaviors. +Array of `AzionFirewallBehaviorItem` objects. Each item uses a discriminated union on `type` to define which behavior is applied and what attributes it requires. -**Properties:** +### `AzionFirewallBehaviorItem` + +A discriminated union — one of: -- `runFunction?: string | number` - Run a serverless function (function name or ID). -- `setWafRuleset?: { wafMode: string; wafId: string | number }` - Set WAF ruleset. -- `setRateLimit?: RateLimitConfig` - Set rate limit configuration. - - `type: 'second' | 'minute' | 'hour'` - Rate limit time window. - - `limitBy: 'clientIp' | 'global' | 'token'` - Rate limit criteria. - - `averageRateLimit: string` - Average rate limit. - - `maximumBurstSize: string` - Maximum burst size. -- `deny?: boolean` - Deny the request. -- `drop?: boolean` - Drop the request. -- `setCustomResponse?: CustomResponseConfig` - Set custom response. - - `statusCode: number | string` - HTTP status code (200-499). - - `contentType: string` - Response content type. - - `contentBody: string` - Response content body. +- `{ type: 'run_function'; attributes: { value: string | number } }` — Run a serverless function (function name or ID). +- `{ type: 'set_waf'; attributes: { mode: 'learning' | 'blocking'; wafId: string | number } }` — Set WAF ruleset and operation mode. +- `{ type: 'set_rate_limit'; attributes: { type: 'second' | 'minute'; limitBy: 'clientIp' | 'global'; averageRateLimit: string; maximumBurstSize: string } }` — Set rate limit configuration. +- `{ type: 'deny' }` — Deny the request. +- `{ type: 'drop' }` — Drop the request. +- `{ type: 'set_custom_response'; attributes: { statusCode: number | string; contentType: string; contentBody: string } }` — Return a custom HTTP response. ### `AzionWaf` diff --git a/packages/config/src/configProcessor/processStrategy/implementations/secure/firewallProcessConfigStrategy.test.ts b/packages/config/src/configProcessor/processStrategy/implementations/secure/firewallProcessConfigStrategy.test.ts index 9e73d1a9..bcfe9e82 100644 --- a/packages/config/src/configProcessor/processStrategy/implementations/secure/firewallProcessConfigStrategy.test.ts +++ b/packages/config/src/configProcessor/processStrategy/implementations/secure/firewallProcessConfigStrategy.test.ts @@ -96,7 +96,7 @@ describe('FirewallProcessConfigStrategy', () => { ], behaviors: [ { type: 'run_function', attributes: { value: 'func-1' } }, - { type: 'set_waf_ruleset', attributes: { mode: 'blocking', wafId: 123 } }, + { type: 'set_waf', attributes: { mode: 'blocking', wafId: 123 } }, { type: 'set_rate_limit', attributes: { @@ -130,7 +130,7 @@ describe('FirewallProcessConfigStrategy', () => { // behaviors expect(rule.behaviors).toEqual([ { type: 'run_function', attributes: { value: 'func-1' } }, - { type: 'set_waf_ruleset', attributes: { mode: 'blocking', waf_id: 123 } }, + { type: 'set_waf', attributes: { mode: 'blocking', waf_id: 123 } }, { type: 'set_rate_limit', attributes: { @@ -255,7 +255,7 @@ describe('FirewallProcessConfigStrategy', () => { variable: 'request_uri', behaviors: [ { type: 'run_function', attributes: { value: 123 } }, - { type: 'set_waf_ruleset', attributes: { mode: 'blocking', wafId: 456 } }, + { type: 'set_waf', attributes: { mode: 'blocking', wafId: 456 } }, { type: 'deny' }, ], }, @@ -270,7 +270,7 @@ describe('FirewallProcessConfigStrategy', () => { expect(rule.behaviors).toEqual([ { type: 'run_function', attributes: { value: 123 } }, - { type: 'set_waf_ruleset', attributes: { mode: 'blocking', waf_id: 456 } }, + { type: 'set_waf', attributes: { mode: 'blocking', waf_id: 456 } }, { type: 'deny' }, ]); }); @@ -374,7 +374,7 @@ describe('FirewallProcessConfigStrategy', () => { active: false, behaviors: [ { type: 'run_function', attributes: { value: '/path/to/f.js' } }, - { type: 'set_waf_ruleset', attributes: { mode: 'learning', waf_id: 999 } }, + { type: 'set_waf', attributes: { mode: 'learning', waf_id: 999 } }, { type: 'set_rate_limit', attributes: { @@ -418,7 +418,7 @@ describe('FirewallProcessConfigStrategy', () => { active: false, behaviors: [ { type: 'run_function', attributes: { value: '/path/to/f.js' } }, - { type: 'set_waf_ruleset', attributes: { mode: 'learning', wafId: 999 } }, + { type: 'set_waf', attributes: { mode: 'learning', wafId: 999 } }, { type: 'set_rate_limit', attributes: { @@ -430,7 +430,10 @@ describe('FirewallProcessConfigStrategy', () => { }, { type: 'deny' }, { type: 'drop' }, - { type: 'set_custom_response', attributes: { statusCode: 403, contentType: 'text/plain', contentBody: 'forbidden' } }, + { + type: 'set_custom_response', + attributes: { statusCode: 403, contentType: 'text/plain', contentBody: 'forbidden' }, + }, ], criteria: [ { variable: 'host', operator: 'is_equal', conditional: 'if', argument: 'bad.com' }, @@ -546,7 +549,7 @@ describe('FirewallProcessConfigStrategy', () => { active: true, behaviors: [ { type: 'run_function', attributes: { value: 999 } }, - { type: 'set_waf_ruleset', attributes: { mode: 'counting', waf_id: 111 } }, + { type: 'set_waf', attributes: { mode: 'counting', waf_id: 111 } }, { type: 'set_custom_response', attributes: { @@ -573,7 +576,7 @@ describe('FirewallProcessConfigStrategy', () => { active: true, behaviors: [ { type: 'run_function', attributes: { value: 999 } }, - { type: 'set_waf_ruleset', attributes: { mode: 'counting', wafId: 111 } }, + { type: 'set_waf', attributes: { mode: 'counting', wafId: 111 } }, { type: 'set_custom_response', attributes: { diff --git a/packages/config/src/configProcessor/processStrategy/implementations/secure/firewallProcessConfigStrategy.ts b/packages/config/src/configProcessor/processStrategy/implementations/secure/firewallProcessConfigStrategy.ts index 757067a6..2cb79d67 100644 --- a/packages/config/src/configProcessor/processStrategy/implementations/secure/firewallProcessConfigStrategy.ts +++ b/packages/config/src/configProcessor/processStrategy/implementations/secure/firewallProcessConfigStrategy.ts @@ -126,9 +126,9 @@ class FirewallProcessConfigStrategy extends ProcessConfigStrategy { behaviors.push(behaviorItem); } - if (behaviorItem.type === 'set_waf_ruleset') { + if (behaviorItem.type === 'set_waf') { behaviors.push({ - type: 'set_waf_ruleset', + type: 'set_waf', attributes: { mode: behaviorItem.attributes.mode, waf_id: behaviorItem.attributes.wafId, @@ -239,9 +239,9 @@ class FirewallProcessConfigStrategy extends ProcessConfigStrategy { attributes: b.attributes, }); break; - case 'set_waf_ruleset': + case 'set_waf': behaviorArray.push({ - type: 'set_waf_ruleset', + type: 'set_waf', attributes: { mode: b.attributes.mode, wafId: b.attributes.waf_id, diff --git a/packages/config/src/configProcessor/schemas/firewall/behaviors/index.ts b/packages/config/src/configProcessor/schemas/firewall/behaviors/index.ts index b2c73485..d568529a 100644 --- a/packages/config/src/configProcessor/schemas/firewall/behaviors/index.ts +++ b/packages/config/src/configProcessor/schemas/firewall/behaviors/index.ts @@ -53,27 +53,27 @@ const setWafRuleSetBehaviorSchema = { properties: { type: { type: 'string', - enum: ['set_waf_ruleset'], - errorMessage: "The 'type' field must be a valid set_waf_ruleset behavior type.", + enum: ['set_waf'], + errorMessage: "The 'type' field must be a valid set_waf behavior type.", }, attributes: { type: 'object', properties: { - wafMode: { + mode: { type: 'string', enum: FIREWALL_WAF_MODES, - errorMessage: `The wafMode must be one of: ${FIREWALL_WAF_MODES.join(', ')}`, + errorMessage: `The mode must be one of: ${FIREWALL_WAF_MODES.join(', ')}`, }, wafId: { type: ['string', 'number'], errorMessage: 'The wafId must be a string or number', }, }, - required: ['wafMode', 'wafId'], + required: ['mode', 'wafId'], additionalProperties: false, errorMessage: { - additionalProperties: 'No additional properties are allowed in the set_waf_ruleset object', - required: "Both 'wafMode' and 'wafId' fields are required in set_waf_ruleset attributes", + additionalProperties: 'No additional properties are allowed in the set_waf object', + required: "Both 'mode' and 'wafId' fields are required in set_waf attributes", }, }, }, diff --git a/packages/config/src/configProcessor/schemas/schemaManifest.ts b/packages/config/src/configProcessor/schemas/schemaManifest.ts index b0fb9b72..674949c9 100644 --- a/packages/config/src/configProcessor/schemas/schemaManifest.ts +++ b/packages/config/src/configProcessor/schemas/schemaManifest.ts @@ -304,7 +304,7 @@ const schemaFirewallRuleBehaviorArguments = { required: ['type', 'limit_by', 'average_rate_limit'], additionalProperties: false, }, - set_waf_ruleset: { + set_waf: { type: 'object', properties: { waf_id: { @@ -320,8 +320,8 @@ const schemaFirewallRuleBehaviorArguments = { required: ['waf_id', 'mode'], additionalProperties: false, errorMessage: { - additionalProperties: 'No additional properties are allowed in the set_waf_ruleset object', - required: "Both 'waf_id' and 'mode' fields are required in set_waf_ruleset", + additionalProperties: 'No additional properties are allowed in the set_waf object', + required: "Both 'waf_id' and 'mode' fields are required in set_waf", }, }, set_custom_response: { @@ -375,7 +375,7 @@ const schemaFirewallRuleBehavior = { { type: 'null' }, { type: 'string' }, schemaFirewallRuleBehaviorArguments.set_rate_limit, - schemaFirewallRuleBehaviorArguments.set_waf_ruleset, + schemaFirewallRuleBehaviorArguments.set_waf, schemaFirewallRuleBehaviorArguments.set_custom_response, ], errorMessage: "The 'target' must be a string, object, or null depending on the behavior.", diff --git a/packages/config/src/constants.ts b/packages/config/src/constants.ts index 1054381b..448bc04b 100644 --- a/packages/config/src/constants.ts +++ b/packages/config/src/constants.ts @@ -93,7 +93,7 @@ export const FIREWALL_BEHAVIOR_NAMES = [ 'deny', 'drop', 'set_rate_limit', - 'set_waf_ruleset', + 'set_waf', 'run_function', 'set_custom_response', ] as const; diff --git a/packages/config/src/types.ts b/packages/config/src/types.ts index b0b4aa54..93b4ae30 100644 --- a/packages/config/src/types.ts +++ b/packages/config/src/types.ts @@ -112,7 +112,7 @@ export type FirewallBehaviorName = | 'deny' | 'drop' | 'set_rate_limit' - | 'set_waf_ruleset' + | 'set_waf' | 'run_function' | 'set_custom_response'; export type FirewallRateLimitType = 'second' | 'minute'; @@ -570,7 +570,7 @@ export type AzionApplication = { */ export type AzionFirewallBehaviorItem = | { type: 'run_function'; attributes: { value: string | number } } - | { type: 'set_waf_ruleset'; attributes: { mode: FirewallWafMode; wafId: string | number } } + | { type: 'set_waf'; attributes: { mode: FirewallWafMode; wafId: string | number } } | { type: 'set_rate_limit'; attributes: { @@ -582,7 +582,10 @@ export type AzionFirewallBehaviorItem = } | { type: 'deny' } | { type: 'drop' } - | { type: 'set_custom_response'; attributes: { statusCode: number | string; contentType: string; contentBody: string } }; + | { + type: 'set_custom_response'; + attributes: { statusCode: number | string; contentType: string; contentBody: string }; + }; /** * Firewall behavior configuration for Azion. From a38e49c86890772be0e289148454dc7f5fa57036 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jos=C3=A9=20Filho?= Date: Thu, 11 Jun 2026 15:33:06 -0300 Subject: [PATCH 7/9] chore: add changeset --- .changeset/free-ravens-jump.md | 9 +++++++++ 1 file changed, 9 insertions(+) create mode 100644 .changeset/free-ravens-jump.md diff --git a/.changeset/free-ravens-jump.md b/.changeset/free-ravens-jump.md new file mode 100644 index 00000000..b522104b --- /dev/null +++ b/.changeset/free-ravens-jump.md @@ -0,0 +1,9 @@ +--- +'@aziontech/config': patch +--- + +refactor(config): reorganize schemas and normalize firewall behavior shape + +- move schema files from helpers/ to a dedicated schemas/ directory +- split monolithic schema into per-feature modules +- rename set_waf_ruleset behavior to set_waf and update docs From d9c832ea271305c55460fad45878e30bff7c7f2d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jos=C3=A9=20Filho?= Date: Thu, 18 Jun 2026 11:26:56 -0300 Subject: [PATCH 8/9] ci: remove token npm --- .github/workflows/release.yml | 1 - 1 file changed, 1 deletion(-) diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 7a2c5142..d5bcfd90 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -35,4 +35,3 @@ jobs: env: GITHUB_TOKEN: ${{ secrets.CUSTOM_GITHUB_TOKEN }} NODE_ENV: 'production' - NPM_TOKEN: ${{ secrets.NPM_TOKEN }} From e014d127a8150f3237527bdaa979005f06f7feb5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jos=C3=A9=20Filho?= Date: Thu, 18 Jun 2026 14:37:03 -0300 Subject: [PATCH 9/9] fix(config): rename limitBy value from clientIp to client_ip and improve firewall behavior replace oneOf with if/then/else in firewallBehaviorSchema so AJV only reports errors from the branch matching the behavior type. Also corrects the FIREWALL_RATE_LIMIT_BY constant, types, examples, and tests to use the snake_case value client_ip. --- packages/config/README.md | 4 ++-- .../helpers/azion.config.example.ts | 2 +- .../firewallProcessConfigStrategy.test.ts | 4 ++-- .../schemas/firewall/behaviors/index.ts | 23 ++++++++++++------- packages/config/src/constants.ts | 2 +- packages/config/src/types.ts | 2 +- 6 files changed, 22 insertions(+), 15 deletions(-) diff --git a/packages/config/README.md b/packages/config/README.md index e712bf98..d34cf6da 100644 --- a/packages/config/README.md +++ b/packages/config/README.md @@ -301,7 +301,7 @@ const config = defineConfig({ type: 'set_rate_limit', attributes: { type: 'second', - limitBy: 'clientIp', + limitBy: 'client_ip', averageRateLimit: '10', maximumBurstSize: '20', }, @@ -849,7 +849,7 @@ A discriminated union — one of: - `{ type: 'run_function'; attributes: { value: string | number } }` — Run a serverless function (function name or ID). - `{ type: 'set_waf'; attributes: { mode: 'learning' | 'blocking'; wafId: string | number } }` — Set WAF ruleset and operation mode. -- `{ type: 'set_rate_limit'; attributes: { type: 'second' | 'minute'; limitBy: 'clientIp' | 'global'; averageRateLimit: string; maximumBurstSize: string } }` — Set rate limit configuration. +- `{ type: 'set_rate_limit'; attributes: { type: 'second' | 'minute'; limitBy: 'client_ip' | 'global'; averageRateLimit: string; maximumBurstSize: string } }` — Set rate limit configuration. - `{ type: 'deny' }` — Deny the request. - `{ type: 'drop' }` — Drop the request. - `{ type: 'set_custom_response'; attributes: { statusCode: number | string; contentType: string; contentBody: string } }` — Return a custom HTTP response. diff --git a/packages/config/src/configProcessor/helpers/azion.config.example.ts b/packages/config/src/configProcessor/helpers/azion.config.example.ts index 85b17cf6..f4616d68 100644 --- a/packages/config/src/configProcessor/helpers/azion.config.example.ts +++ b/packages/config/src/configProcessor/helpers/azion.config.example.ts @@ -529,7 +529,7 @@ const config: AzionConfig = { attributes: { type: 'second', averageRateLimit: '1', - limitBy: 'clientIp', + limitBy: 'client_ip', maximumBurstSize: '1', }, }, diff --git a/packages/config/src/configProcessor/processStrategy/implementations/secure/firewallProcessConfigStrategy.test.ts b/packages/config/src/configProcessor/processStrategy/implementations/secure/firewallProcessConfigStrategy.test.ts index bcfe9e82..6f52e35e 100644 --- a/packages/config/src/configProcessor/processStrategy/implementations/secure/firewallProcessConfigStrategy.test.ts +++ b/packages/config/src/configProcessor/processStrategy/implementations/secure/firewallProcessConfigStrategy.test.ts @@ -101,7 +101,7 @@ describe('FirewallProcessConfigStrategy', () => { type: 'set_rate_limit', attributes: { type: 'minute', - limitBy: 'clientIp', + limitBy: 'client_ip', averageRateLimit: '60', maximumBurstSize: '20', }, @@ -135,7 +135,7 @@ describe('FirewallProcessConfigStrategy', () => { type: 'set_rate_limit', attributes: { type: 'minute', - limit_by: 'clientIp', + limit_by: 'client_ip', average_rate_limit: '60', maximum_burst_size: '20', }, diff --git a/packages/config/src/configProcessor/schemas/firewall/behaviors/index.ts b/packages/config/src/configProcessor/schemas/firewall/behaviors/index.ts index d568529a..1c4c58d7 100644 --- a/packages/config/src/configProcessor/schemas/firewall/behaviors/index.ts +++ b/packages/config/src/configProcessor/schemas/firewall/behaviors/index.ts @@ -178,14 +178,21 @@ const runFunctionBehaviorSchema = { }; const firewallBehaviorSchema = { - oneOf: [ - runFunctionBehaviorSchema, - setWafRuleSetBehaviorSchema, - setRateLimitBehaviorSchema, - noArgsBehaviorSchema, - setCustomResponseBehaviorSchema, - ], - errorMessage: 'Each behavior must match one of the valid behavior formats.', + if: { type: 'object', properties: { type: { const: 'run_function' } }, required: ['type'] }, + then: runFunctionBehaviorSchema, + else: { + if: { type: 'object', properties: { type: { const: 'set_waf' } }, required: ['type'] }, + then: setWafRuleSetBehaviorSchema, + else: { + if: { type: 'object', properties: { type: { const: 'set_rate_limit' } }, required: ['type'] }, + then: setRateLimitBehaviorSchema, + else: { + if: { type: 'object', properties: { type: { const: 'set_custom_response' } }, required: ['type'] }, + then: setCustomResponseBehaviorSchema, + else: noArgsBehaviorSchema, + }, + }, + }, }; export default firewallBehaviorSchema; diff --git a/packages/config/src/constants.ts b/packages/config/src/constants.ts index 448bc04b..b884398f 100644 --- a/packages/config/src/constants.ts +++ b/packages/config/src/constants.ts @@ -100,7 +100,7 @@ export const FIREWALL_BEHAVIOR_NAMES = [ export const FIREWALL_RATE_LIMIT_TYPES = ['second', 'minute'] as const; -export const FIREWALL_RATE_LIMIT_BY = ['clientIp', 'global'] as const; +export const FIREWALL_RATE_LIMIT_BY = ['client_ip', 'global'] as const; export const FIREWALL_WAF_MODES = ['learning', 'blocking'] as const; diff --git a/packages/config/src/types.ts b/packages/config/src/types.ts index 93b4ae30..8e343e4a 100644 --- a/packages/config/src/types.ts +++ b/packages/config/src/types.ts @@ -116,7 +116,7 @@ export type FirewallBehaviorName = | 'run_function' | 'set_custom_response'; export type FirewallRateLimitType = 'second' | 'minute'; -export type FirewallRateLimitBy = 'clientIp' | 'global'; +export type FirewallRateLimitBy = 'client_ip' | 'global'; export type FirewallWafMode = 'learning' | 'blocking'; export type FirewallVariable = | 'header_accept'