cloudflare-expression-lint 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CLAUDE.md +57 -0
- package/LICENSE +21 -0
- package/README.md +375 -0
- package/dist/cli.d.ts +22 -0
- package/dist/cli.js +363 -0
- package/dist/cli.js.map +1 -0
- package/dist/eslint-plugin.d.ts +67 -0
- package/dist/eslint-plugin.js +211 -0
- package/dist/eslint-plugin.js.map +1 -0
- package/dist/index.d.ts +21 -0
- package/dist/index.js +19 -0
- package/dist/index.js.map +1 -0
- package/dist/lexer.d.ts +11 -0
- package/dist/lexer.js +416 -0
- package/dist/lexer.js.map +1 -0
- package/dist/parser.d.ts +16 -0
- package/dist/parser.js +320 -0
- package/dist/parser.js.map +1 -0
- package/dist/schemas/fields.d.ts +44 -0
- package/dist/schemas/fields.js +282 -0
- package/dist/schemas/fields.js.map +1 -0
- package/dist/schemas/functions.d.ts +33 -0
- package/dist/schemas/functions.js +261 -0
- package/dist/schemas/functions.js.map +1 -0
- package/dist/schemas/operators.d.ts +28 -0
- package/dist/schemas/operators.js +37 -0
- package/dist/schemas/operators.js.map +1 -0
- package/dist/types.d.ts +149 -0
- package/dist/types.js +38 -0
- package/dist/types.js.map +1 -0
- package/dist/validator.d.ts +18 -0
- package/dist/validator.js +420 -0
- package/dist/validator.js.map +1 -0
- package/dist/yaml-scanner.d.ts +97 -0
- package/dist/yaml-scanner.js +175 -0
- package/dist/yaml-scanner.js.map +1 -0
- package/package.json +80 -0
package/CLAUDE.md
ADDED
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
# cloudflare-expression-lint
|
|
2
|
+
|
|
3
|
+
## What This Is
|
|
4
|
+
A TypeScript parser, validator, and linter for Cloudflare Rules Language expressions. It catches errors in Cloudflare expressions before they reach `terraform apply`.
|
|
5
|
+
|
|
6
|
+
## Project Structure
|
|
7
|
+
- `src/lexer.ts` — Tokenizer (string → Token[])
|
|
8
|
+
- `src/parser.ts` — Recursive-descent parser (Token[] → AST)
|
|
9
|
+
- `src/validator.ts` — Semantic validator (AST → Diagnostic[])
|
|
10
|
+
- `src/yaml-scanner.ts` — YAML file scanner with phase inference
|
|
11
|
+
- `src/cli.ts` — CLI entry point
|
|
12
|
+
- `src/types.ts` — All type definitions
|
|
13
|
+
- `src/schemas/fields.ts` — Field registry (211+ fields with types, deprecation, phase availability)
|
|
14
|
+
- `src/schemas/functions.ts` — Function registry (25+ functions with context restrictions)
|
|
15
|
+
- `src/schemas/operators.ts` — Operator definitions
|
|
16
|
+
- `src/__tests__/` — Test suite (203+ tests)
|
|
17
|
+
|
|
18
|
+
## Commands
|
|
19
|
+
- `npm test` — Run tests (vitest)
|
|
20
|
+
- `npm run build` — Build TypeScript to dist/
|
|
21
|
+
- `node dist/cli.js -e 'EXPRESSION'` — Validate a single expression
|
|
22
|
+
- `node dist/cli.js config/**/*.yaml` — Scan YAML files
|
|
23
|
+
|
|
24
|
+
## How to Add a New Field
|
|
25
|
+
Add an entry to the `FIELDS` array in `src/schemas/fields.ts`:
|
|
26
|
+
```typescript
|
|
27
|
+
{ name: 'cf.new_field', type: 'String' },
|
|
28
|
+
// With deprecation:
|
|
29
|
+
{ name: 'old.field', type: 'String', deprecated: true, replacement: 'new.field' },
|
|
30
|
+
// With phase restriction:
|
|
31
|
+
{ name: 'http.response.new', type: 'String', phases: ['http_response_headers_transform'] },
|
|
32
|
+
```
|
|
33
|
+
|
|
34
|
+
## How to Add a New Function
|
|
35
|
+
Add an entry to the `FUNCTIONS` array in `src/schemas/functions.ts`:
|
|
36
|
+
```typescript
|
|
37
|
+
{
|
|
38
|
+
name: 'new_function',
|
|
39
|
+
params: [{ name: 'input', type: 'String' }],
|
|
40
|
+
returnType: 'String',
|
|
41
|
+
contexts: ['all'], // or ['filter'], ['rewrite_url', 'rewrite_header'], etc.
|
|
42
|
+
maxPerExpression: 1, // optional usage limit
|
|
43
|
+
},
|
|
44
|
+
```
|
|
45
|
+
|
|
46
|
+
## Expression Types
|
|
47
|
+
- `filter` — Boolean expressions (the "when" condition in rules)
|
|
48
|
+
- `rewrite_url` — URL rewrite value expressions (e.g., regex_replace result)
|
|
49
|
+
- `rewrite_header` — Header value expressions
|
|
50
|
+
- `redirect_target` — Redirect target URL expressions
|
|
51
|
+
|
|
52
|
+
## Key Design Decisions
|
|
53
|
+
- Schemas are data, not code — field/function definitions are in simple arrays
|
|
54
|
+
- Parser is custom (not using wirefilter WASM) for better error messages
|
|
55
|
+
- ESLint is NOT a dependency — this is a standalone tool
|
|
56
|
+
- The validator produces warnings for deprecated fields, errors for unknown/invalid ones
|
|
57
|
+
- YAML scanner infers Cloudflare phase from parent YAML keys (e.g., `waf_rules` → `http_request_firewall_custom`)
|
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Troy Jones
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,375 @@
|
|
|
1
|
+
# cloudflare-expression-lint
|
|
2
|
+
|
|
3
|
+
A parser, validator, and linter for [Cloudflare Rules Language](https://developers.cloudflare.com/ruleset-engine/rules-language/) expressions with phase-aware field and function checking.
|
|
4
|
+
|
|
5
|
+
Catches errors **before** `terraform apply` — no API calls required.
|
|
6
|
+
|
|
7
|
+
## Features
|
|
8
|
+
|
|
9
|
+
- **Full expression parser** — lexer + recursive-descent parser for the Cloudflare wirefilter expression syntax
|
|
10
|
+
- **211+ known fields** with type information
|
|
11
|
+
- **Deprecated field detection** — warns on legacy fields like `ip.geoip.country` with replacement suggestions
|
|
12
|
+
- **Phase-aware validation** — knows which fields are available in which Cloudflare phase (e.g., response fields only in response phases)
|
|
13
|
+
- **Function context validation** — `regex_replace()` is only valid in rewrite/redirect contexts, not filter expressions
|
|
14
|
+
- **Function usage limits** — enforces max 1 `regex_replace()` / `wildcard_replace()` per expression
|
|
15
|
+
- **YAML scanner** — auto-detects expressions in YAML config files and infers the Cloudflare phase from context
|
|
16
|
+
- **CLI tool** — validate expressions from the command line or CI/CD pipelines
|
|
17
|
+
- **Programmatic API** — use as a library in your own tools
|
|
18
|
+
|
|
19
|
+
## Installation
|
|
20
|
+
|
|
21
|
+
```bash
|
|
22
|
+
npm install cloudflare-expression-lint
|
|
23
|
+
```
|
|
24
|
+
|
|
25
|
+
Or run directly with npx:
|
|
26
|
+
|
|
27
|
+
```bash
|
|
28
|
+
npx cloudflare-expression-lint config/**/*.yaml
|
|
29
|
+
```
|
|
30
|
+
|
|
31
|
+
## CLI Usage
|
|
32
|
+
|
|
33
|
+
### Validate YAML files
|
|
34
|
+
|
|
35
|
+
```bash
|
|
36
|
+
# Scan all YAML files for expressions
|
|
37
|
+
cf-expr-lint config/**/*.yaml
|
|
38
|
+
|
|
39
|
+
# Scan with JSON output (for CI integration)
|
|
40
|
+
cf-expr-lint --format json config/**/*.yaml
|
|
41
|
+
|
|
42
|
+
# Only show errors (suppress warnings)
|
|
43
|
+
cf-expr-lint --quiet config/**/*.yaml
|
|
44
|
+
```
|
|
45
|
+
|
|
46
|
+
### Validate a single expression
|
|
47
|
+
|
|
48
|
+
```bash
|
|
49
|
+
# Filter expression (default)
|
|
50
|
+
cf-expr-lint -e '(http.host eq "example.com")'
|
|
51
|
+
|
|
52
|
+
# Rewrite expression
|
|
53
|
+
cf-expr-lint -e 'regex_replace(http.request.uri.path, "^/old/", "/new/")' -t rewrite_url
|
|
54
|
+
|
|
55
|
+
# With phase validation
|
|
56
|
+
cf-expr-lint -e 'http.response.code eq 200' -p http_request_firewall_custom
|
|
57
|
+
# ✗ [field-not-in-phase]: Field "http.response.code" is not available in phase "http_request_firewall_custom"
|
|
58
|
+
|
|
59
|
+
# From stdin
|
|
60
|
+
echo '(ip.src.country in {"US" "JP"})' | cf-expr-lint --stdin
|
|
61
|
+
```
|
|
62
|
+
|
|
63
|
+
### Custom YAML key mappings (CLI)
|
|
64
|
+
|
|
65
|
+
By default, the scanner only looks for the `expression` key (the standard
|
|
66
|
+
Cloudflare Terraform provider attribute). If your YAML uses other key
|
|
67
|
+
names for expressions, tell the scanner about them:
|
|
68
|
+
|
|
69
|
+
```bash
|
|
70
|
+
# Add custom expression keys and phase mappings via flags
|
|
71
|
+
cf-expr-lint \
|
|
72
|
+
--expr-key rewrite_expression:rewrite_url:http_request_transform \
|
|
73
|
+
--expr-key source_url_expression:filter:http_request_dynamic_redirect \
|
|
74
|
+
--phase-map waf_rules:http_request_firewall_custom \
|
|
75
|
+
--phase-map transform_rules:http_request_late_transform \
|
|
76
|
+
config/**/*.yaml
|
|
77
|
+
```
|
|
78
|
+
|
|
79
|
+
The `--expr-key` format is `key_name:expression_type[:phase]`.
|
|
80
|
+
The `--phase-map` format is `yaml_parent_key:cloudflare_phase`.
|
|
81
|
+
|
|
82
|
+
Both merge with the built-in defaults — your custom mappings extend them,
|
|
83
|
+
they don't replace them.
|
|
84
|
+
|
|
85
|
+
### Config file
|
|
86
|
+
|
|
87
|
+
For projects with many custom mappings, use a `.cf-expr-lint.json` config
|
|
88
|
+
file in your project root (auto-detected) or specified with `--config`:
|
|
89
|
+
|
|
90
|
+
```json
|
|
91
|
+
{
|
|
92
|
+
"expressionKeys": {
|
|
93
|
+
"rewrite_expression": { "type": "rewrite_url", "phaseHint": "http_request_transform" },
|
|
94
|
+
"source_url_expression": { "type": "filter", "phaseHint": "http_request_dynamic_redirect" },
|
|
95
|
+
"target_url_expression": { "type": "redirect_target", "phaseHint": "http_request_dynamic_redirect" }
|
|
96
|
+
},
|
|
97
|
+
"phaseMappings": {
|
|
98
|
+
"waf_rules": "http_request_firewall_custom",
|
|
99
|
+
"custom_rules": "http_request_firewall_custom",
|
|
100
|
+
"configuration_rules": "http_config_settings",
|
|
101
|
+
"transform_request_header_rules": "http_request_late_transform",
|
|
102
|
+
"transform_response_header_rules": "http_response_headers_transform",
|
|
103
|
+
"transform_url_rewrite_rules": "http_request_transform"
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
```
|
|
107
|
+
|
|
108
|
+
Then just run: `cf-expr-lint config/**/*.yaml`
|
|
109
|
+
|
|
110
|
+
### CLI Options
|
|
111
|
+
|
|
112
|
+
| Option | Short | Description |
|
|
113
|
+
|--------|-------|-------------|
|
|
114
|
+
| `--expression` | `-e` | Validate a single expression string |
|
|
115
|
+
| `--stdin` | | Read expression from stdin |
|
|
116
|
+
| `--type` | `-t` | Expression type: `filter` (default), `rewrite_url`, `rewrite_header`, `redirect_target` |
|
|
117
|
+
| `--phase` | `-p` | Cloudflare phase for field validation |
|
|
118
|
+
| `--config` | `-c` | Path to config file (JSON) with custom mappings |
|
|
119
|
+
| `--expr-key` | | Add expression key mapping: `key:type[:phase]` (repeatable) |
|
|
120
|
+
| `--phase-map` | | Add phase mapping: `yaml_key:phase` (repeatable) |
|
|
121
|
+
| `--format` | `-f` | Output format: `text` (default), `json` |
|
|
122
|
+
| `--quiet` | `-q` | Only show errors (suppress warnings) |
|
|
123
|
+
| `--help` | `-h` | Show help |
|
|
124
|
+
|
|
125
|
+
## Programmatic API
|
|
126
|
+
|
|
127
|
+
```typescript
|
|
128
|
+
import { validate, parse, tokenize } from 'cloudflare-expression-lint';
|
|
129
|
+
|
|
130
|
+
// Validate an expression
|
|
131
|
+
const result = validate('(http.host eq "example.com")', {
|
|
132
|
+
expressionType: 'filter',
|
|
133
|
+
phase: 'http_request_firewall_custom',
|
|
134
|
+
});
|
|
135
|
+
|
|
136
|
+
console.log(result.valid); // true
|
|
137
|
+
console.log(result.diagnostics); // []
|
|
138
|
+
|
|
139
|
+
// Validate with warnings
|
|
140
|
+
const result2 = validate('(ip.geoip.country eq "US")', {
|
|
141
|
+
expressionType: 'filter',
|
|
142
|
+
});
|
|
143
|
+
// result2.valid === true (warnings don't make it invalid)
|
|
144
|
+
// result2.diagnostics[0].code === 'deprecated-field'
|
|
145
|
+
// result2.diagnostics[0].message === 'Field "ip.geoip.country" is deprecated. Use "ip.src.country" instead'
|
|
146
|
+
|
|
147
|
+
// Parse to AST
|
|
148
|
+
const ast = parse('http.host eq "example.com"');
|
|
149
|
+
console.log(ast.kind); // 'Comparison'
|
|
150
|
+
|
|
151
|
+
// Tokenize
|
|
152
|
+
const tokens = tokenize('http.host eq "example.com"');
|
|
153
|
+
```
|
|
154
|
+
|
|
155
|
+
### YAML Scanner
|
|
156
|
+
|
|
157
|
+
```typescript
|
|
158
|
+
import { scanYaml } from 'cloudflare-expression-lint/yaml-scanner';
|
|
159
|
+
import { readFileSync } from 'fs';
|
|
160
|
+
|
|
161
|
+
const content = readFileSync('config/zones/example.yaml', 'utf-8');
|
|
162
|
+
const result = scanYaml(content, 'example.yaml');
|
|
163
|
+
|
|
164
|
+
for (const expr of result.expressions) {
|
|
165
|
+
if (!expr.result.valid) {
|
|
166
|
+
console.error(`${expr.file} → ${expr.yamlPath}: ${expr.result.diagnostics[0].message}`);
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
```
|
|
170
|
+
|
|
171
|
+
## Supported Expression Syntax
|
|
172
|
+
|
|
173
|
+
This tool supports the full Cloudflare Rules Language syntax:
|
|
174
|
+
|
|
175
|
+
### Operators
|
|
176
|
+
|
|
177
|
+
| Type | English | C-like |
|
|
178
|
+
|------|---------|--------|
|
|
179
|
+
| Equal | `eq` | `==` |
|
|
180
|
+
| Not equal | `ne` | `!=` |
|
|
181
|
+
| Less than | `lt` | `<` |
|
|
182
|
+
| Less/equal | `le` | `<=` |
|
|
183
|
+
| Greater than | `gt` | `>` |
|
|
184
|
+
| Greater/equal | `ge` | `>=` |
|
|
185
|
+
| Contains | `contains` | |
|
|
186
|
+
| Wildcard | `wildcard` | |
|
|
187
|
+
| Strict wildcard | `strict wildcard` | |
|
|
188
|
+
| Regex match | `matches` | `~` |
|
|
189
|
+
| Set membership | `in` | |
|
|
190
|
+
| AND | `and` | `&&` |
|
|
191
|
+
| OR | `or` | `\|\|` |
|
|
192
|
+
| NOT | `not` | `!` |
|
|
193
|
+
| XOR | `xor` | `^^` |
|
|
194
|
+
|
|
195
|
+
### Value Types
|
|
196
|
+
|
|
197
|
+
- **Strings**: `"value"` with `\"` and `\\` escaping
|
|
198
|
+
- **Integers**: `42`, `0`, `396507`
|
|
199
|
+
- **Booleans**: `true`, `false`
|
|
200
|
+
- **IP Addresses**: `1.2.3.4`, with CIDR (`1.2.3.0/24`) in `in` lists
|
|
201
|
+
- **Named Lists**: `$list_name`, `$cf.malware`
|
|
202
|
+
- **In-lists**: `{"US" "JP"}`, `{8000..8009}`, `{1.2.3.0/24}`
|
|
203
|
+
|
|
204
|
+
### Functions
|
|
205
|
+
|
|
206
|
+
All standard Cloudflare functions are supported, with context-aware validation:
|
|
207
|
+
|
|
208
|
+
| Function | Available In |
|
|
209
|
+
|----------|-------------|
|
|
210
|
+
| `lower()`, `upper()`, `len()`, `starts_with()`, `ends_with()`, `contains()` | All contexts |
|
|
211
|
+
| `concat()`, `substring()`, `url_decode()` | All contexts |
|
|
212
|
+
| `any()`, `all()`, `has_key()`, `has_value()` | All contexts |
|
|
213
|
+
| `lookup_json_string()`, `lookup_json_integer()` | All contexts |
|
|
214
|
+
| `regex_replace()` | Rewrite, redirect (max 1 per expression) |
|
|
215
|
+
| `wildcard_replace()` | Rewrite, redirect (max 1 per expression) |
|
|
216
|
+
| `to_string()`, `encode_base64()`, `uuidv4()`, `sha256()` | Rewrite/transform only |
|
|
217
|
+
| `cidr()`, `cidr6()` | Filter only |
|
|
218
|
+
|
|
219
|
+
## Diagnostic Codes
|
|
220
|
+
|
|
221
|
+
| Code | Severity | Description |
|
|
222
|
+
|------|----------|-------------|
|
|
223
|
+
| `parse-error` | error | Syntax error in expression |
|
|
224
|
+
| `unknown-field` | error | Field name not recognized |
|
|
225
|
+
| `unknown-function` | error | Function name not recognized |
|
|
226
|
+
| `field-not-in-phase` | error | Field not available in the specified Cloudflare phase |
|
|
227
|
+
| `function-not-in-context` | error | Function not available in the expression context (filter vs rewrite) |
|
|
228
|
+
| `function-max-exceeded` | error | Function used more times than allowed |
|
|
229
|
+
| `operator-type-mismatch` | error | Operator not compatible with field type (e.g., `contains` on IP) |
|
|
230
|
+
| `deprecated-field` | warning | Field is deprecated; replacement suggested |
|
|
231
|
+
| `expression-too-long` | warning | Expression exceeds 4096 character limit |
|
|
232
|
+
| `header-key-not-lowercase` | warning | Header map key should be lowercase |
|
|
233
|
+
| `invalid-list-name` | warning | Named list name doesn't match Cloudflare naming rules |
|
|
234
|
+
| `invalid-cidr-mask` | error | CIDR mask out of valid range |
|
|
235
|
+
| `invalid-wildcard-pattern` | warning | Wildcard contains `**` (prohibited) |
|
|
236
|
+
| `invalid-list-name` | warning | Named list name doesn't follow Cloudflare naming rules |
|
|
237
|
+
| `empty-in-list` | warning | Empty `in {}` list will never match |
|
|
238
|
+
| `too-many-regex` | warning | More than 64 regex patterns per expression |
|
|
239
|
+
| `prefer-bare-boolean` | info | Prefer `ssl` over `ssl == true` |
|
|
240
|
+
|
|
241
|
+
## How Mappings Work
|
|
242
|
+
|
|
243
|
+
The scanner needs to know two things about each YAML file:
|
|
244
|
+
|
|
245
|
+
1. **Which keys contain expressions?** — By default, only `expression` (the Terraform attribute name).
|
|
246
|
+
2. **What Cloudflare phase does an expression belong to?** — Inferred from YAML parent keys.
|
|
247
|
+
|
|
248
|
+
Both are **extensible** — your custom mappings always merge with the built-in
|
|
249
|
+
defaults. You never lose the defaults unless you explicitly opt out.
|
|
250
|
+
|
|
251
|
+
### Programmatic API
|
|
252
|
+
|
|
253
|
+
```typescript
|
|
254
|
+
import { scanYaml } from 'cloudflare-expression-lint';
|
|
255
|
+
|
|
256
|
+
const result = scanYaml(yamlContent, 'config.yaml', {
|
|
257
|
+
// These MERGE with the built-in defaults
|
|
258
|
+
expressionKeys: {
|
|
259
|
+
'rewrite_expression': { type: 'rewrite_url', phaseHint: 'http_request_transform' },
|
|
260
|
+
'source_url_expression': { type: 'filter', phaseHint: 'http_request_dynamic_redirect' },
|
|
261
|
+
},
|
|
262
|
+
phaseMappings: {
|
|
263
|
+
'waf_rules': 'http_request_firewall_custom',
|
|
264
|
+
'my_transform_rules': 'http_request_transform',
|
|
265
|
+
},
|
|
266
|
+
});
|
|
267
|
+
|
|
268
|
+
// Inspect defaults
|
|
269
|
+
import { getDefaultExpressionKeys, getDefaultPhaseMappings } from 'cloudflare-expression-lint';
|
|
270
|
+
console.log(getDefaultExpressionKeys()); // { expression: { type: 'filter' } }
|
|
271
|
+
console.log(getDefaultPhaseMappings()); // { cache_rules: '...', http_request_firewall_custom: '...', ... }
|
|
272
|
+
```
|
|
273
|
+
|
|
274
|
+
### Built-in Phase Mappings
|
|
275
|
+
|
|
276
|
+
The defaults include all Cloudflare phase names as self-mappings plus common shorthands:
|
|
277
|
+
|
|
278
|
+
| YAML Key | Phase |
|
|
279
|
+
|----------|-------|
|
|
280
|
+
| `http_request_firewall_custom` | `http_request_firewall_custom` |
|
|
281
|
+
| `http_ratelimit` | `http_ratelimit` |
|
|
282
|
+
| `http_request_cache_settings` | `http_request_cache_settings` |
|
|
283
|
+
| `http_request_transform` | `http_request_transform` |
|
|
284
|
+
| `http_request_late_transform` | `http_request_late_transform` |
|
|
285
|
+
| `http_response_headers_transform` | `http_response_headers_transform` |
|
|
286
|
+
| `cache_rules` | `http_request_cache_settings` |
|
|
287
|
+
| `rate_limit_rules` | `http_ratelimit` |
|
|
288
|
+
| `single_redirects` | `http_request_dynamic_redirect` |
|
|
289
|
+
| `origin_rules` | `http_request_origin` |
|
|
290
|
+
|
|
291
|
+
If you need to **replace** all defaults instead of merging, pass
|
|
292
|
+
`replaceExpressionKeys: true` or `replacePhaseMappings: true`.
|
|
293
|
+
|
|
294
|
+
### ESLint Plugin
|
|
295
|
+
|
|
296
|
+
```javascript
|
|
297
|
+
// eslint.config.js (flat config)
|
|
298
|
+
import cfExprLint from 'cloudflare-expression-lint/eslint-plugin';
|
|
299
|
+
|
|
300
|
+
export default [
|
|
301
|
+
{
|
|
302
|
+
files: ['config/**/*.yaml'],
|
|
303
|
+
plugins: { 'cf-expr': cfExprLint },
|
|
304
|
+
rules: {
|
|
305
|
+
'cf-expr/validate-expression': ['error', {
|
|
306
|
+
// Custom mappings (merged with defaults)
|
|
307
|
+
customKeyMappings: {
|
|
308
|
+
'rewrite_expression': 'rewrite_url',
|
|
309
|
+
'source_url_expression': 'filter',
|
|
310
|
+
},
|
|
311
|
+
customPhaseMappings: {
|
|
312
|
+
'waf_rules': 'http_request_firewall_custom',
|
|
313
|
+
},
|
|
314
|
+
}],
|
|
315
|
+
},
|
|
316
|
+
},
|
|
317
|
+
];
|
|
318
|
+
```
|
|
319
|
+
|
|
320
|
+
## CI/CD Integration
|
|
321
|
+
|
|
322
|
+
### GitLab CI
|
|
323
|
+
|
|
324
|
+
```yaml
|
|
325
|
+
lint-expressions:
|
|
326
|
+
stage: validate
|
|
327
|
+
script:
|
|
328
|
+
- npx cloudflare-expression-lint config/**/*.yaml
|
|
329
|
+
allow_failure: false
|
|
330
|
+
```
|
|
331
|
+
|
|
332
|
+
### GitHub Actions
|
|
333
|
+
|
|
334
|
+
```yaml
|
|
335
|
+
- name: Lint Cloudflare expressions
|
|
336
|
+
run: npx cloudflare-expression-lint config/**/*.yaml
|
|
337
|
+
```
|
|
338
|
+
|
|
339
|
+
### Pre-commit Hook
|
|
340
|
+
|
|
341
|
+
```bash
|
|
342
|
+
#!/bin/sh
|
|
343
|
+
npx cloudflare-expression-lint $(git diff --cached --name-only --diff-filter=ACM -- '*.yaml' '*.yml')
|
|
344
|
+
```
|
|
345
|
+
|
|
346
|
+
## Extending the Schema
|
|
347
|
+
|
|
348
|
+
The field and function registries are defined in TypeScript files under `src/schemas/`:
|
|
349
|
+
|
|
350
|
+
- **`fields.ts`** — All known fields with types, deprecation status, and phase availability
|
|
351
|
+
- **`functions.ts`** — All known functions with parameter types, return types, context restrictions, and usage limits
|
|
352
|
+
- **`operators.ts`** — All comparison and logical operators with type constraints
|
|
353
|
+
|
|
354
|
+
To add a new field or function, edit the relevant schema file and add a new entry to the array. The tool will automatically pick it up.
|
|
355
|
+
|
|
356
|
+
## Architecture
|
|
357
|
+
|
|
358
|
+
```
|
|
359
|
+
src/
|
|
360
|
+
├── lexer.ts # Tokenizer: string → Token[]
|
|
361
|
+
├── parser.ts # Parser: Token[] → AST (recursive descent)
|
|
362
|
+
├── validator.ts # Validator: AST → Diagnostic[] (semantic analysis)
|
|
363
|
+
├── yaml-scanner.ts # YAML file scanner with phase inference
|
|
364
|
+
├── cli.ts # CLI entry point
|
|
365
|
+
├── types.ts # Shared type definitions
|
|
366
|
+
├── index.ts # Public API exports
|
|
367
|
+
└── schemas/
|
|
368
|
+
├── fields.ts # 211+ field definitions
|
|
369
|
+
├── functions.ts # 25+ function definitions
|
|
370
|
+
└── operators.ts # Operator definitions
|
|
371
|
+
```
|
|
372
|
+
|
|
373
|
+
## License
|
|
374
|
+
|
|
375
|
+
MIT
|
package/dist/cli.d.ts
ADDED
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* CLI for cloudflare-expression-lint.
|
|
4
|
+
*
|
|
5
|
+
* Usage:
|
|
6
|
+
* cf-expr-lint [options] <files...>
|
|
7
|
+
* cf-expr-lint --expression "http.host eq \"test.com\""
|
|
8
|
+
* cf-expr-lint --config .cf-expr-lint.json config/**\/*.yaml
|
|
9
|
+
*
|
|
10
|
+
* Options:
|
|
11
|
+
* --expression, -e Validate a single expression string
|
|
12
|
+
* --stdin Read expression from stdin
|
|
13
|
+
* --type, -t Expression type: filter, rewrite_url, rewrite_header, redirect_target
|
|
14
|
+
* --phase, -p Cloudflare phase (e.g., http_request_firewall_custom)
|
|
15
|
+
* --config, -c Path to config file (.json) with custom mappings
|
|
16
|
+
* --expr-key Add expression key mapping: key:type[:phase]
|
|
17
|
+
* --phase-map Add phase mapping: yaml_key:phase_name
|
|
18
|
+
* --format, -f Output format: text (default), json
|
|
19
|
+
* --quiet, -q Only output errors (suppress warnings)
|
|
20
|
+
* --help, -h Show this help message
|
|
21
|
+
*/
|
|
22
|
+
export {};
|