flareguard 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.
Files changed (64) hide show
  1. package/Cargo.lock +2962 -0
  2. package/Cargo.toml +66 -0
  3. package/LICENSE +21 -0
  4. package/README.md +115 -0
  5. package/bin/flareguard.js +78 -0
  6. package/package.json +50 -0
  7. package/scripts/postinstall.mjs +83 -0
  8. package/src/bindings/ast_scanner.rs +950 -0
  9. package/src/bindings/cli.rs +49 -0
  10. package/src/bindings/jsonc.rs +166 -0
  11. package/src/bindings/mod.rs +7 -0
  12. package/src/bindings/reporter.rs +328 -0
  13. package/src/bindings/types.rs +129 -0
  14. package/src/bindings/validator.rs +227 -0
  15. package/src/bindings/wrangler.rs +647 -0
  16. package/src/cli.rs +77 -0
  17. package/src/lib.rs +7 -0
  18. package/src/main.rs +451 -0
  19. package/src/origin/cli.rs +92 -0
  20. package/src/origin/cloudflare.rs +170 -0
  21. package/src/origin/confidence.rs +320 -0
  22. package/src/origin/crtsh.rs +144 -0
  23. package/src/origin/dns.rs +177 -0
  24. package/src/origin/enumerator.rs +271 -0
  25. package/src/origin/error.rs +19 -0
  26. package/src/origin/mock.rs +320 -0
  27. package/src/origin/mod.rs +19 -0
  28. package/src/origin/models.rs +167 -0
  29. package/src/origin/prober.rs +260 -0
  30. package/src/origin/remediation.rs +73 -0
  31. package/src/origin/report.rs +625 -0
  32. package/src/origin/scanner.rs +217 -0
  33. package/src/secrets/cli.rs +94 -0
  34. package/src/secrets/env_parser.rs +464 -0
  35. package/src/secrets/ignore.rs +139 -0
  36. package/src/secrets/mod.rs +14 -0
  37. package/src/secrets/report/json_format.rs +97 -0
  38. package/src/secrets/report/mod.rs +48 -0
  39. package/src/secrets/report/sarif.rs +225 -0
  40. package/src/secrets/report/text.rs +105 -0
  41. package/src/secrets/rules/builtin.rs +280 -0
  42. package/src/secrets/rules/entropy.rs +66 -0
  43. package/src/secrets/rules/mod.rs +7 -0
  44. package/src/secrets/rules/types.rs +183 -0
  45. package/src/secrets/scanner.rs +444 -0
  46. package/src/zone/cli.rs +111 -0
  47. package/src/zone/client/cf_client.rs +405 -0
  48. package/src/zone/client/mod.rs +5 -0
  49. package/src/zone/client/provider.rs +12 -0
  50. package/src/zone/mock_data.rs +481 -0
  51. package/src/zone/mod.rs +125 -0
  52. package/src/zone/models/audit.rs +194 -0
  53. package/src/zone/models/cloudflare.rs +252 -0
  54. package/src/zone/models/mod.rs +7 -0
  55. package/src/zone/models/sarif.rs +89 -0
  56. package/src/zone/reporters/html_rep.rs +345 -0
  57. package/src/zone/reporters/json_rep.rs +7 -0
  58. package/src/zone/reporters/mod.rs +9 -0
  59. package/src/zone/reporters/sarif_rep.rs +100 -0
  60. package/src/zone/reporters/terminal.rs +322 -0
  61. package/src/zone/rules/definitions.rs +201 -0
  62. package/src/zone/rules/evaluator.rs +484 -0
  63. package/src/zone/rules/mod.rs +5 -0
  64. package/src/zone/scoring.rs +104 -0
@@ -0,0 +1,647 @@
1
+ use crate::bindings::jsonc::parse_jsonc;
2
+ use crate::bindings::types::{BindingType, DeclaredBinding};
3
+ use std::collections::{BTreeMap, HashMap};
4
+ use std::error::Error;
5
+ use std::fs;
6
+ use std::path::{Path, PathBuf};
7
+
8
+ /// Locate a wrangler configuration file (`wrangler.jsonc`, `wrangler.json`, `wrangler.toml`).
9
+ pub fn find_wrangler_config(start_dir: &Path) -> Option<PathBuf> {
10
+ let candidate_names = ["wrangler.jsonc", "wrangler.json", "wrangler.toml"];
11
+ let mut current = start_dir.to_path_buf();
12
+
13
+ for _ in 0..4 {
14
+ for name in &candidate_names {
15
+ let candidate = current.join(name);
16
+ if candidate.is_file() {
17
+ return Some(candidate);
18
+ }
19
+ }
20
+ if let Some(parent) = current.parent() {
21
+ current = parent.to_path_buf();
22
+ } else {
23
+ break;
24
+ }
25
+ }
26
+ None
27
+ }
28
+
29
+ /// Result of parsing a Wrangler configuration file.
30
+ #[derive(Debug, Clone)]
31
+ pub struct WranglerConfig {
32
+ pub file_path: PathBuf,
33
+ pub root_bindings: Vec<DeclaredBinding>,
34
+ pub environments: BTreeMap<String, Vec<DeclaredBinding>>,
35
+ }
36
+
37
+ impl WranglerConfig {
38
+ /// Retrieve bindings for a specific environment (inheriting and overriding root bindings).
39
+ pub fn get_bindings_for_env(
40
+ &self,
41
+ env_name: Option<&str>,
42
+ ) -> Result<Vec<DeclaredBinding>, Box<dyn Error + Send + Sync>> {
43
+ let mut map: HashMap<String, DeclaredBinding> = HashMap::new();
44
+
45
+ // Load root bindings first
46
+ for b in &self.root_bindings {
47
+ map.insert(b.name.clone(), b.clone());
48
+ }
49
+
50
+ if let Some(env) = env_name {
51
+ if let Some(env_bindings) = self.environments.get(env) {
52
+ for b in env_bindings {
53
+ map.insert(b.name.clone(), b.clone());
54
+ }
55
+ } else {
56
+ let available: Vec<String> = self.environments.keys().cloned().collect();
57
+ return Err(format!(
58
+ "Environment '{}' not found in {}. Available environments: {:?}",
59
+ env,
60
+ self.file_path.display(),
61
+ available
62
+ )
63
+ .into());
64
+ }
65
+ }
66
+
67
+ let mut list: Vec<DeclaredBinding> = map.into_values().collect();
68
+ list.sort_by(|a, b| a.name.cmp(&b.name));
69
+ Ok(list)
70
+ }
71
+ }
72
+
73
+ /// Parses a wrangler configuration file (TOML, JSON, or JSONC).
74
+ pub fn parse_wrangler_config(path: &Path) -> Result<WranglerConfig, Box<dyn Error + Send + Sync>> {
75
+ let content = fs::read_to_string(path)
76
+ .map_err(|e| format!("Failed to read wrangler config {}: {}", path.display(), e))?;
77
+ let path_str = path.to_string_lossy().to_string();
78
+
79
+ let ext = path.extension().and_then(|e| e.to_str()).unwrap_or("");
80
+ if ext == "toml" {
81
+ parse_wrangler_toml(&content, &path_str, path)
82
+ } else {
83
+ // Assume json or jsonc
84
+ parse_wrangler_json(&content, &path_str, path)
85
+ }
86
+ }
87
+
88
+ /// Parse TOML wrangler configuration
89
+ fn parse_wrangler_toml(
90
+ content: &str,
91
+ file_path_str: &str,
92
+ path: &Path,
93
+ ) -> Result<WranglerConfig, Box<dyn Error + Send + Sync>> {
94
+ let toml_val: toml::Value = toml::from_str(content)
95
+ .map_err(|e| format!("Failed to parse TOML in {}: {}", path.display(), e))?;
96
+
97
+ // Convert to serde_json::Value for unified extraction logic
98
+ let json_val: serde_json::Value = serde_json::to_value(toml_val)?;
99
+
100
+ extract_wrangler_config_from_json(&json_val, file_path_str, path)
101
+ }
102
+
103
+ /// Parse JSON / JSONC wrangler configuration
104
+ fn parse_wrangler_json(
105
+ content: &str,
106
+ file_path_str: &str,
107
+ path: &Path,
108
+ ) -> Result<WranglerConfig, Box<dyn Error + Send + Sync>> {
109
+ let json_val = parse_jsonc(content)
110
+ .map_err(|e| format!("Failed to parse JSON/JSONC in {}: {}", path.display(), e))?;
111
+
112
+ extract_wrangler_config_from_json(&json_val, file_path_str, path)
113
+ }
114
+
115
+ /// Unified extraction from serde_json::Value
116
+ fn extract_wrangler_config_from_json(
117
+ val: &serde_json::Value,
118
+ file_path_str: &str,
119
+ path: &Path,
120
+ ) -> Result<WranglerConfig, Box<dyn Error + Send + Sync>> {
121
+ let root_bindings = extract_bindings_from_object(val, file_path_str, "root");
122
+
123
+ let mut environments = BTreeMap::new();
124
+ if let Some(env_map) = val.get("env").and_then(|e| e.as_object()) {
125
+ for (env_name, env_val) in env_map {
126
+ let env_bindings = extract_bindings_from_object(env_val, file_path_str, env_name);
127
+ environments.insert(env_name.clone(), env_bindings);
128
+ }
129
+ }
130
+
131
+ Ok(WranglerConfig {
132
+ file_path: path.to_path_buf(),
133
+ root_bindings,
134
+ environments,
135
+ })
136
+ }
137
+
138
+ /// Extract bindings from a single environment or root config object
139
+ pub fn extract_bindings_from_object(
140
+ val: &serde_json::Value,
141
+ file_path: &str,
142
+ env_name: &str,
143
+ ) -> Vec<DeclaredBinding> {
144
+ let mut bindings = Vec::new();
145
+ let obj = match val.as_object() {
146
+ Some(o) => o,
147
+ None => return bindings,
148
+ };
149
+
150
+ // 1. vars
151
+ if let Some(vars) = obj.get("vars").and_then(|v| v.as_object()) {
152
+ for (k, v) in vars {
153
+ bindings.push(DeclaredBinding {
154
+ name: k.clone(),
155
+ binding_type: BindingType::Var,
156
+ file: file_path.to_string(),
157
+ environment: env_name.to_string(),
158
+ details: Some(format!("value: {}", v)),
159
+ });
160
+ }
161
+ }
162
+
163
+ // 2. kv_namespaces
164
+ if let Some(kvs) = obj.get("kv_namespaces").and_then(|k| k.as_array()) {
165
+ for kv in kvs {
166
+ if let Some(binding) = kv.get("binding").and_then(|b| b.as_str()) {
167
+ let id = kv
168
+ .get("id")
169
+ .and_then(|i| i.as_str())
170
+ .map(|s| format!("id: {}", s));
171
+ bindings.push(DeclaredBinding {
172
+ name: binding.to_string(),
173
+ binding_type: BindingType::KvNamespace,
174
+ file: file_path.to_string(),
175
+ environment: env_name.to_string(),
176
+ details: id,
177
+ });
178
+ }
179
+ }
180
+ }
181
+
182
+ // 3. d1_databases
183
+ if let Some(d1s) = obj.get("d1_databases").and_then(|d| d.as_array()) {
184
+ for d1 in d1s {
185
+ if let Some(binding) = d1.get("binding").and_then(|b| b.as_str()) {
186
+ let db_name = d1
187
+ .get("database_name")
188
+ .and_then(|n| n.as_str())
189
+ .map(|s| format!("db: {}", s));
190
+ bindings.push(DeclaredBinding {
191
+ name: binding.to_string(),
192
+ binding_type: BindingType::D1Database,
193
+ file: file_path.to_string(),
194
+ environment: env_name.to_string(),
195
+ details: db_name,
196
+ });
197
+ }
198
+ }
199
+ }
200
+
201
+ // 4. r2_buckets
202
+ if let Some(r2s) = obj.get("r2_buckets").and_then(|r| r.as_array()) {
203
+ for r2 in r2s {
204
+ if let Some(binding) = r2.get("binding").and_then(|b| b.as_str()) {
205
+ let bucket = r2
206
+ .get("bucket_name")
207
+ .and_then(|n| n.as_str())
208
+ .map(|s| format!("bucket: {}", s));
209
+ bindings.push(DeclaredBinding {
210
+ name: binding.to_string(),
211
+ binding_type: BindingType::R2Bucket,
212
+ file: file_path.to_string(),
213
+ environment: env_name.to_string(),
214
+ details: bucket,
215
+ });
216
+ }
217
+ }
218
+ }
219
+
220
+ // 5. vectorize
221
+ if let Some(vecs) = obj.get("vectorize") {
222
+ if let Some(arr) = vecs.as_array() {
223
+ for v in arr {
224
+ if let Some(binding) = v.get("binding").and_then(|b| b.as_str()) {
225
+ let idx = v
226
+ .get("index_name")
227
+ .and_then(|n| n.as_str())
228
+ .map(|s| format!("index: {}", s));
229
+ bindings.push(DeclaredBinding {
230
+ name: binding.to_string(),
231
+ binding_type: BindingType::Vectorize,
232
+ file: file_path.to_string(),
233
+ environment: env_name.to_string(),
234
+ details: idx,
235
+ });
236
+ }
237
+ }
238
+ } else if let Some(v_obj) = vecs.as_object()
239
+ && let Some(binding) = v_obj.get("binding").and_then(|b| b.as_str()) {
240
+ let idx = v_obj
241
+ .get("index_name")
242
+ .and_then(|n| n.as_str())
243
+ .map(|s| format!("index: {}", s));
244
+ bindings.push(DeclaredBinding {
245
+ name: binding.to_string(),
246
+ binding_type: BindingType::Vectorize,
247
+ file: file_path.to_string(),
248
+ environment: env_name.to_string(),
249
+ details: idx,
250
+ });
251
+ }
252
+ }
253
+
254
+ // 6. hyperdrive
255
+ if let Some(hds) = obj.get("hyperdrive").and_then(|h| h.as_array()) {
256
+ for hd in hds {
257
+ if let Some(binding) = hd.get("binding").and_then(|b| b.as_str()) {
258
+ let id = hd
259
+ .get("id")
260
+ .and_then(|i| i.as_str())
261
+ .map(|s| format!("id: {}", s));
262
+ bindings.push(DeclaredBinding {
263
+ name: binding.to_string(),
264
+ binding_type: BindingType::Hyperdrive,
265
+ file: file_path.to_string(),
266
+ environment: env_name.to_string(),
267
+ details: id,
268
+ });
269
+ }
270
+ }
271
+ }
272
+
273
+ // 7. services
274
+ if let Some(services) = obj.get("services").and_then(|s| s.as_array()) {
275
+ for s in services {
276
+ if let Some(binding) = s.get("binding").and_then(|b| b.as_str()) {
277
+ let svc = s
278
+ .get("service")
279
+ .and_then(|n| n.as_str())
280
+ .map(|srv| format!("service: {}", srv));
281
+ bindings.push(DeclaredBinding {
282
+ name: binding.to_string(),
283
+ binding_type: BindingType::Service,
284
+ file: file_path.to_string(),
285
+ environment: env_name.to_string(),
286
+ details: svc,
287
+ });
288
+ }
289
+ }
290
+ }
291
+
292
+ // 8. analytics_engine_datasets
293
+ if let Some(aeds) = obj
294
+ .get("analytics_engine_datasets")
295
+ .and_then(|a| a.as_array())
296
+ {
297
+ for a in aeds {
298
+ if let Some(binding) = a.get("binding").and_then(|b| b.as_str()) {
299
+ let ds = a
300
+ .get("dataset")
301
+ .and_then(|d| d.as_str())
302
+ .map(|s| format!("dataset: {}", s));
303
+ bindings.push(DeclaredBinding {
304
+ name: binding.to_string(),
305
+ binding_type: BindingType::AnalyticsEngine,
306
+ file: file_path.to_string(),
307
+ environment: env_name.to_string(),
308
+ details: ds,
309
+ });
310
+ }
311
+ }
312
+ }
313
+
314
+ // 9. durable_objects
315
+ if let Some(do_val) = obj.get("durable_objects").and_then(|d| d.as_object())
316
+ && let Some(bindings_arr) = do_val.get("bindings").and_then(|b| b.as_array()) {
317
+ for b in bindings_arr {
318
+ let name = b
319
+ .get("name")
320
+ .or_else(|| b.get("binding"))
321
+ .and_then(|n| n.as_str());
322
+ if let Some(binding_name) = name {
323
+ let cls = b
324
+ .get("class_name")
325
+ .and_then(|c| c.as_str())
326
+ .map(|s| format!("class: {}", s));
327
+ bindings.push(DeclaredBinding {
328
+ name: binding_name.to_string(),
329
+ binding_type: BindingType::DurableObject,
330
+ file: file_path.to_string(),
331
+ environment: env_name.to_string(),
332
+ details: cls,
333
+ });
334
+ }
335
+ }
336
+ }
337
+
338
+ // 10. queues (producers)
339
+ if let Some(queues) = obj.get("queues").and_then(|q| q.as_object())
340
+ && let Some(producers) = queues.get("producers").and_then(|p| p.as_array()) {
341
+ for p in producers {
342
+ if let Some(binding) = p.get("binding").and_then(|b| b.as_str()) {
343
+ let q_name = p
344
+ .get("queue")
345
+ .and_then(|q| q.as_str())
346
+ .map(|s| format!("queue: {}", s));
347
+ bindings.push(DeclaredBinding {
348
+ name: binding.to_string(),
349
+ binding_type: BindingType::QueueProducer,
350
+ file: file_path.to_string(),
351
+ environment: env_name.to_string(),
352
+ details: q_name,
353
+ });
354
+ }
355
+ }
356
+ }
357
+
358
+ // 11. workflows
359
+ if let Some(wfs) = obj.get("workflows").and_then(|w| w.as_array()) {
360
+ for wf in wfs {
361
+ let name = wf
362
+ .get("binding")
363
+ .or_else(|| wf.get("name"))
364
+ .and_then(|n| n.as_str());
365
+ if let Some(binding_name) = name {
366
+ let cls = wf
367
+ .get("class_name")
368
+ .and_then(|c| c.as_str())
369
+ .map(|s| format!("class: {}", s));
370
+ bindings.push(DeclaredBinding {
371
+ name: binding_name.to_string(),
372
+ binding_type: BindingType::Workflow,
373
+ file: file_path.to_string(),
374
+ environment: env_name.to_string(),
375
+ details: cls,
376
+ });
377
+ }
378
+ }
379
+ }
380
+
381
+ // 12. ai
382
+ if let Some(ai_val) = obj.get("ai") {
383
+ let binding_name = if let Some(ai_obj) = ai_val.as_object() {
384
+ ai_obj
385
+ .get("binding")
386
+ .and_then(|b| b.as_str())
387
+ .unwrap_or("AI")
388
+ } else {
389
+ "AI"
390
+ };
391
+ bindings.push(DeclaredBinding {
392
+ name: binding_name.to_string(),
393
+ binding_type: BindingType::Ai,
394
+ file: file_path.to_string(),
395
+ environment: env_name.to_string(),
396
+ details: None,
397
+ });
398
+ }
399
+
400
+ // 13. browser
401
+ if let Some(browser_val) = obj.get("browser") {
402
+ let binding_name = if let Some(b_obj) = browser_val.as_object() {
403
+ b_obj
404
+ .get("binding")
405
+ .and_then(|b| b.as_str())
406
+ .unwrap_or("MYBROWSER")
407
+ } else {
408
+ "BROWSER"
409
+ };
410
+ bindings.push(DeclaredBinding {
411
+ name: binding_name.to_string(),
412
+ binding_type: BindingType::Browser,
413
+ file: file_path.to_string(),
414
+ environment: env_name.to_string(),
415
+ details: None,
416
+ });
417
+ }
418
+
419
+ // 14. send_email
420
+ if let Some(email_val) = obj.get("send_email") {
421
+ if let Some(arr) = email_val.as_array() {
422
+ for em in arr {
423
+ let name = em
424
+ .get("name")
425
+ .or_else(|| em.get("binding"))
426
+ .and_then(|n| n.as_str());
427
+ if let Some(binding_name) = name {
428
+ bindings.push(DeclaredBinding {
429
+ name: binding_name.to_string(),
430
+ binding_type: BindingType::SendEmail,
431
+ file: file_path.to_string(),
432
+ environment: env_name.to_string(),
433
+ details: None,
434
+ });
435
+ }
436
+ }
437
+ } else if let Some(em_obj) = email_val.as_object() {
438
+ let name = em_obj
439
+ .get("name")
440
+ .or_else(|| em_obj.get("binding"))
441
+ .and_then(|n| n.as_str());
442
+ if let Some(binding_name) = name {
443
+ bindings.push(DeclaredBinding {
444
+ name: binding_name.to_string(),
445
+ binding_type: BindingType::SendEmail,
446
+ file: file_path.to_string(),
447
+ environment: env_name.to_string(),
448
+ details: None,
449
+ });
450
+ }
451
+ }
452
+ }
453
+
454
+ // 15. mtls_certificates
455
+ if let Some(mtls) = obj.get("mtls_certificates").and_then(|m| m.as_array()) {
456
+ for cert in mtls {
457
+ if let Some(binding) = cert.get("binding").and_then(|b| b.as_str()) {
458
+ let cert_id = cert
459
+ .get("certificate_id")
460
+ .and_then(|i| i.as_str())
461
+ .map(|s| format!("cert: {}", s));
462
+ bindings.push(DeclaredBinding {
463
+ name: binding.to_string(),
464
+ binding_type: BindingType::MtlsCertificate,
465
+ file: file_path.to_string(),
466
+ environment: env_name.to_string(),
467
+ details: cert_id,
468
+ });
469
+ }
470
+ }
471
+ }
472
+
473
+ // 16. pipelines
474
+ if let Some(pipes) = obj.get("pipelines").and_then(|p| p.as_array()) {
475
+ for pipe in pipes {
476
+ if let Some(binding) = pipe.get("binding").and_then(|b| b.as_str()) {
477
+ let p_name = pipe
478
+ .get("pipeline")
479
+ .and_then(|n| n.as_str())
480
+ .map(|s| format!("pipeline: {}", s));
481
+ bindings.push(DeclaredBinding {
482
+ name: binding.to_string(),
483
+ binding_type: BindingType::Pipeline,
484
+ file: file_path.to_string(),
485
+ environment: env_name.to_string(),
486
+ details: p_name,
487
+ });
488
+ }
489
+ }
490
+ }
491
+
492
+ // 17. assets
493
+ if let Some(assets_val) = obj.get("assets").and_then(|a| a.as_object())
494
+ && let Some(binding) = assets_val.get("binding").and_then(|b| b.as_str()) {
495
+ let dir = assets_val
496
+ .get("directory")
497
+ .and_then(|d| d.as_str())
498
+ .map(|s| format!("dir: {}", s));
499
+ bindings.push(DeclaredBinding {
500
+ name: binding.to_string(),
501
+ binding_type: BindingType::Assets,
502
+ file: file_path.to_string(),
503
+ environment: env_name.to_string(),
504
+ details: dir,
505
+ });
506
+ }
507
+
508
+ // 18. secrets (if defined as array of secret names)
509
+ if let Some(secrets) = obj.get("secrets").and_then(|s| s.as_array()) {
510
+ for s in secrets {
511
+ if let Some(name) = s.as_str() {
512
+ bindings.push(DeclaredBinding {
513
+ name: name.to_string(),
514
+ binding_type: BindingType::Secret,
515
+ file: file_path.to_string(),
516
+ environment: env_name.to_string(),
517
+ details: None,
518
+ });
519
+ }
520
+ }
521
+ }
522
+
523
+ bindings
524
+ }
525
+
526
+ #[cfg(test)]
527
+ mod tests {
528
+ use super::*;
529
+
530
+ #[test]
531
+ fn test_parse_wrangler_jsonc_all_bindings() {
532
+ let jsonc = r#"
533
+ {
534
+ "name": "full-worker",
535
+ "vars": {
536
+ "API_KEY": "secret123",
537
+ "ENV": "production"
538
+ },
539
+ "kv_namespaces": [
540
+ { "binding": "MY_KV", "id": "kv123" }
541
+ ],
542
+ "d1_databases": [
543
+ { "binding": "DB", "database_name": "prod-db", "database_id": "d1-123" }
544
+ ],
545
+ "r2_buckets": [
546
+ { "binding": "BUCKET", "bucket_name": "my-bucket" }
547
+ ],
548
+ "ai": { "binding": "AI" },
549
+ "browser": { "binding": "HEADLESS" },
550
+ "durable_objects": {
551
+ "bindings": [
552
+ { "name": "RATE_LIMITER", "class_name": "RateLimiter" }
553
+ ]
554
+ },
555
+ "queues": {
556
+ "producers": [
557
+ { "binding": "NOTIFY_QUEUE", "queue": "notifications" }
558
+ ]
559
+ },
560
+ "services": [
561
+ { "binding": "AUTH_SVC", "service": "auth-worker" }
562
+ ],
563
+ "vectorize": [
564
+ { "binding": "DOC_INDEX", "index_name": "docs" }
565
+ ],
566
+ "hyperdrive": [
567
+ { "binding": "POSTGRES", "id": "hd123" }
568
+ ],
569
+ "analytics_engine_datasets": [
570
+ { "binding": "METRICS", "dataset": "telemetry" }
571
+ ],
572
+ "workflows": [
573
+ { "name": "ONBOARDING", "class_name": "OnboardingWorkflow" }
574
+ ],
575
+ "env": {
576
+ "staging": {
577
+ "vars": {
578
+ "ENV": "staging",
579
+ "STAGING_FLAG": "true"
580
+ },
581
+ "kv_namespaces": [
582
+ { "binding": "STAGE_KV", "id": "kv-stage" }
583
+ ]
584
+ }
585
+ }
586
+ }
587
+ "#;
588
+ let config =
589
+ parse_wrangler_json(jsonc, "wrangler.jsonc", Path::new("wrangler.jsonc")).unwrap();
590
+ let root_bindings = config.get_bindings_for_env(None).unwrap();
591
+ let names: Vec<&str> = root_bindings.iter().map(|b| b.name.as_str()).collect();
592
+
593
+ assert!(names.contains(&"API_KEY"));
594
+ assert!(names.contains(&"MY_KV"));
595
+ assert!(names.contains(&"DB"));
596
+ assert!(names.contains(&"BUCKET"));
597
+ assert!(names.contains(&"AI"));
598
+ assert!(names.contains(&"HEADLESS"));
599
+ assert!(names.contains(&"RATE_LIMITER"));
600
+ assert!(names.contains(&"NOTIFY_QUEUE"));
601
+ assert!(names.contains(&"AUTH_SVC"));
602
+ assert!(names.contains(&"DOC_INDEX"));
603
+ assert!(names.contains(&"POSTGRES"));
604
+ assert!(names.contains(&"METRICS"));
605
+ assert!(names.contains(&"ONBOARDING"));
606
+
607
+ // Check staging env overrides & inheritance
608
+ let staging_bindings = config.get_bindings_for_env(Some("staging")).unwrap();
609
+ let staging_names: Vec<&str> = staging_bindings.iter().map(|b| b.name.as_str()).collect();
610
+ assert!(staging_names.contains(&"STAGING_FLAG"));
611
+ assert!(staging_names.contains(&"STAGE_KV"));
612
+ assert!(staging_names.contains(&"MY_KV")); // Inherited
613
+ }
614
+
615
+ #[test]
616
+ fn test_parse_wrangler_toml() {
617
+ let toml_str = r#"
618
+ name = "toml-worker"
619
+
620
+ [vars]
621
+ API_SECRET = "abc"
622
+
623
+ [[kv_namespaces]]
624
+ binding = "KV_DATA"
625
+ id = "kv-data-id"
626
+
627
+ [[d1_databases]]
628
+ binding = "D1_MAIN"
629
+ database_name = "d1-main"
630
+
631
+ [env.preview]
632
+ vars = { PREVIEW_ONLY = "yes" }
633
+ "#;
634
+ let config =
635
+ parse_wrangler_toml(toml_str, "wrangler.toml", Path::new("wrangler.toml")).unwrap();
636
+ let root_bindings = config.get_bindings_for_env(None).unwrap();
637
+ let names: Vec<&str> = root_bindings.iter().map(|b| b.name.as_str()).collect();
638
+ assert!(names.contains(&"API_SECRET"));
639
+ assert!(names.contains(&"KV_DATA"));
640
+ assert!(names.contains(&"D1_MAIN"));
641
+
642
+ let preview_bindings = config.get_bindings_for_env(Some("preview")).unwrap();
643
+ let preview_names: Vec<&str> = preview_bindings.iter().map(|b| b.name.as_str()).collect();
644
+ assert!(preview_names.contains(&"PREVIEW_ONLY"));
645
+ assert!(preview_names.contains(&"API_SECRET"));
646
+ }
647
+ }