timonel 2.14.0-beta.1 → 3.0.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/README.md CHANGED
@@ -17,18 +17,102 @@ directory.
17
17
 
18
18
  ## ✨ Key Features
19
19
 
20
+ ### Core Capabilities
21
+
20
22
  - **🔒 Type-safe API** with strict TypeScript and cdk8s constructs
21
- - **🎯 Type-Safe Helm Helpers** with 9 composable template helpers (`helmIf`, `helmRange`,
22
- `helmWith`, `helmInclude`, etc.)
23
23
  - **🔧 Flexible resource creation** with built-in methods and `addManifest()` for custom resources
24
24
  - **🌍 Multi-environment support** with automatic values files generation
25
25
  - **☂️ Umbrella Charts** for managing multiple subcharts as a single unit
26
- - **🛠️ Enhanced Helm Helpers** with environment, GitOps, observability, and validation helpers
27
- - **☁️ Cloud integrations**:
28
- - **AWS**: EBS/EFS StorageClass, ALB Ingress, IRSA ServiceAccount, ECR, and Karpenter
29
- - **🛡️ Security-first approach** with NetworkPolicies and best practices
30
- - **⚡ Minimal CLI** (`tl`) for scaffolding and chart generation
31
- - **📦 Flexible subchart templates** supporting cdk8s and cdk8s-plus-33
26
+ - **⚡ Minimal CLI** (`tl`) for scaffolding, synthesis, validation, and deployment
27
+ - **📦 Flexible subchart templates** supporting both cdk8s and cdk8s-plus-33
28
+
29
+ ### Type-Safe Helm Helpers (v3.0+)
30
+
31
+ #### ValuesRef System (NEW in v3.0 - RECOMMENDED)
32
+
33
+ Type-safe proxy-based values references with full IDE support:
34
+
35
+ - Import: `import { valuesRef } from 'timonel'`
36
+ - **Comparison operators**: `eq`, `ne`, `gt`, `ge`, `lt`, `le`
37
+ - **Logical operators**: `not`, `and`, `or`
38
+ - **String functions**: `quote`, `upper`, `lower`, `trim`, `replace`, `contains`
39
+ - **Default values**: `default()`
40
+ - **Type checking**: `kindIs`, `hasKey`
41
+ - **YAML functions**: `toYaml`, `toJson`, `nindent`, `indent`
42
+ - **Field-level conditionals**: `v.if()`, `v.ifElse()` - Complex conditional logic
43
+ - **Range loops**: `v.range()` - Type-safe iteration
44
+ - **Context switching**: `v.with()` - Scoped value access
45
+
46
+ #### Composable Helpers
47
+
48
+ Template definition and inclusion helpers:
49
+
50
+ - `helmInclude`, `helmDefine`, `helmVar`, `helmBlock`, `helmComment`, `helmFragment`
51
+ - `template`, `include`, `quote`, `indent`
52
+
53
+ #### Value Reference Helpers
54
+
55
+ Useful string-based utilities (no ValuesRef equivalent):
56
+
57
+ - `requiredValuesRef` - Required value with validation
58
+ - `numberRef`, `boolRef`, `floatRef` - Type-cast references (int, bool, float64)
59
+ - `base64Ref` - Base64 encoding
60
+
61
+ #### Legacy Helpers (NOT RECOMMENDED)
62
+
63
+ **⚠️ Use ValuesRef system instead:**
64
+
65
+ - `valuesRef(path)` → use `v.path` (ValuesRef system)
66
+ - `stringRef()` → use `v.quote()` (ValuesRef system)
67
+ - `defaultRef()` → use `v.default()` (ValuesRef system)
68
+ - `jsonRef()` → use `v.toJson()` (ValuesRef system)
69
+ - `conditionalRef()` → use `v.if()` (ValuesRef system)
70
+ - `helmIf`, `helmIfSimple` → use `v.if()` (ValuesRef system)
71
+ - `helmRange` → use `v.range()` (ValuesRef system)
72
+ - `helmWith` → use `v.with()` (ValuesRef system)
73
+ - `helmIfElseIf` → use `v.if()` with nested conditions
74
+
75
+ ### Enhanced Helm Helpers
76
+
77
+ - **Environment Helpers**: `envRef`, `envDefault`, `envRequired`, `envFromSecret`, `envFromConfigMap`
78
+ - **GitOps Helpers**: `gitBranch`, `gitCommit`, `gitTag`, `gitopsAnnotations`
79
+ - **Observability Helpers**: `prometheusAnnotations`, `datadogAnnotations`, `tracingAnnotations`
80
+ - **Validation Helpers**: `validateRequired`, `validatePattern`, `validateRange`, `validateEnum`
81
+ - **Standard Helpers**: 40+ built-in Helm helpers (chart.name, chart.fullname, chart.labels, etc.)
82
+
83
+ ### Cloud Integrations
84
+
85
+ - **AWS Resources**:
86
+ - EBS/EFS StorageClass with encryption and performance options
87
+ - ALB Ingress with SSL/TLS and health checks
88
+ - IRSA ServiceAccount for pod-level IAM roles
89
+ - ECR integration
90
+ - Karpenter NodePool, NodeClaim, and EC2NodeClass
91
+ - **Karpenter Features**:
92
+ - Disruption budgets and consolidation policies
93
+ - Instance type selection and requirements
94
+ - Spot instance support
95
+ - Custom AMI and user data
96
+
97
+ ### Security & Validation
98
+
99
+ - **🛡️ Security-first approach**:
100
+ - Input validation (CWE-20, CWE-22/23)
101
+ - Path traversal prevention
102
+ - Command injection prevention (CWE-78/77/88)
103
+ - Log injection protection (CWE-117)
104
+ - Code injection prevention (CWE-94)
105
+ - **NetworkPolicy support** for pod-level network isolation
106
+ - **Helm chart validation** with `validateHelmYaml`
107
+ - **SecurityUtils** for path validation and sanitization
108
+
109
+ ### Developer Experience
110
+
111
+ - **Structured logging** with Pino (JSON format, performance tracking)
112
+ - **Environment variables loader** for external configuration
113
+ - **YAML serialization** with Helm template preservation
114
+ - **TypeScript strict mode** with all compiler checks enabled
115
+ - **Comprehensive error handling** with detailed messages
32
116
 
33
117
  ## 🚀 Quick Start
34
118
 
@@ -153,18 +237,114 @@ const umbrellaConfig = {
153
237
  export const umbrella = new UmbrellaChartTemplate(umbrellaConfig);
154
238
  ```
155
239
 
156
- ### Type-Safe Helm Helpers
240
+ ### Using Type-Safe Helm Helpers
241
+
242
+ #### ValuesRef System (Recommended)
243
+
244
+ The new ValuesRef system provides a type-safe, proxy-based approach to Helm values with full IDE support.
245
+
246
+ **⚠️ Important:** This is completely different from the legacy `valuesRef(path: string)` helper.
247
+ The new system uses generics and returns a proxy object with methods.
248
+
249
+ ```typescript
250
+ import { valuesRef } from 'timonel';
251
+
252
+ interface MyValues {
253
+ replicaCount: number;
254
+ image: { repository: string; tag: string };
255
+ autoscaling: { enabled: boolean; minReplicas: number };
256
+ }
257
+
258
+ const v = valuesRef<MyValues>();
259
+
260
+ // Type-safe value references with IDE autocomplete
261
+ const replicas = v.replicaCount; // {{ .Values.replicaCount }}
262
+ const imageTag = v.image.tag; // {{ .Values.image.tag }}
263
+
264
+ // Comparison operators
265
+ const isProd = v.environment.eq('production'); // eq .Values.environment "production"
266
+ const hasReplicas = v.replicaCount.gt(1); // gt .Values.replicaCount 1
267
+
268
+ // Logical operators
269
+ const notEnabled = v.autoscaling.enabled.not(); // not .Values.autoscaling.enabled
270
+
271
+ // String functions
272
+ const upperEnv = v.environment.upper(); // .Values.environment | upper
273
+ const quotedTag = v.image.tag.quote(); // .Values.image.tag | quote
274
+
275
+ // Default values
276
+ const port = v.port.default(8080); // {{ .Values.port | default 8080 }}
277
+
278
+ // Field-level conditionals
279
+ const deployment = {
280
+ spec: {
281
+ replicas: v.if(notEnabled, v.replicaCount), // Conditionally include field
282
+ },
283
+ };
284
+
285
+ // Range loops
286
+ const envVars = v.env.range((item, index) => ({
287
+ name: item.name,
288
+ value: item.value,
289
+ }));
290
+
291
+ // Context switching
292
+ const dbConfig = v.database.with((db) => ({
293
+ host: db.host,
294
+ port: db.port,
295
+ }));
296
+ ```
297
+
298
+ #### Template Composition Helpers
299
+
300
+ Use these helpers for template definitions and inclusions:
301
+
302
+ ```typescript
303
+ import { helmInclude, helmDefine, helmFragment } from 'timonel';
304
+
305
+ // Template inclusion with pipe
306
+ const labels = helmInclude('chart.labels', '.', { pipe: 'nindent 4' });
307
+
308
+ // Define a named template
309
+ const myTemplate = helmDefine('myapp.config', {
310
+ key: 'value',
311
+ });
312
+
313
+ // Combine multiple constructs
314
+ const combined = helmFragment(helmInclude('chart.labels', '.'), { customKey: 'customValue' });
315
+ ```
316
+
317
+ #### Legacy Flow Control (NOT RECOMMENDED)
318
+
319
+ **⚠️ These are legacy and NOT RECOMMENDED. Use ValuesRef system (v.if, v.range, v.with)
320
+ instead:**
321
+
322
+ ```typescript
323
+ // ❌ OLD WAY - string-based valuesRef (no type safety)
324
+ const oldRef = valuesRef('.Values.production');
325
+
326
+ // ✅ NEW WAY - ValuesRef system (type-safe)
327
+ const v = valuesRef<MyValues>();
328
+ const newRef = v.production; // {{ .Values.production }}
329
+
330
+ // ❌ OLD WAY - helmIf, helmRange, helmWith (string-based)
331
+ const config = helmIf('.Values.production', { replicas: 5 }, { replicas: 1 });
332
+
333
+ // ✅ NEW WAY - v.if(), v.range(), v.with() (type-safe)
334
+ const config = v.if(v.production, { replicas: 5 });
335
+ ```
157
336
 
158
- Timonel provides 9 composable, type-safe helpers for Helm template generation: `helmIf`,
159
- `helmRange`, `helmWith`, `helmInclude`, `helmDefine`, `helmVar`, `helmBlock`, `helmComment`, and
160
- `helmFragment`.
337
+ **Why ValuesRef is Better:**
161
338
 
162
- **Benefits:**
339
+ - ✅ **100% Type-Safe** - Catch errors at compile time with TypeScript generics
340
+ - ✅ **No Raw Strings** - Eliminate manual template interpolation and typos
341
+ - ✅ **Full IDE Support** - Autocomplete, type hints, and refactoring support
342
+ - ✅ **Proxy-based** - Chainable methods for complex logic
343
+ - ✅ **Composable** - Nest and combine operations naturally
344
+ - ❌ **Legacy helpers** - String-based, error-prone, no IDE support
163
345
 
164
- - 100% Type-Safe - catch errors at compile time
165
- - No Raw Strings - eliminate manual template interpolation
166
- - ✅ Composable - nest and combine helpers freely
167
- - ✅ Full IDE Support - autocomplete and type hints
346
+ **Migration:** Replace `valuesRef(path)` with `v.path`. Replace `helmIf`, `helmRange`, `helmWith`
347
+ with `v.if()`, `v.range()`, `v.with()`.
168
348
 
169
349
  **Learn more:** See the
170
350
  [Type-Safe Helm Helpers Guide](https://github.com/KenkoGeek/timonel/wiki/Helm-Helpers-System) for
@@ -200,10 +380,10 @@ If you get `Error: Cannot find module 'cdk8s'` when running `tl umbrella synth`:
200
380
  "version": "1.0.0",
201
381
  "type": "module",
202
382
  "dependencies": {
203
- "cdk8s": "^2.70.16",
204
- "cdk8s-plus-33": "^2.3.8",
205
- "constructs": "^10.4.2",
206
- "timonel": "^2.11.0"
383
+ "cdk8s": "^2.70.28",
384
+ "cdk8s-plus-33": "^2.4.6",
385
+ "constructs": "^10.4.3",
386
+ "timonel": "^3.0.0-beta.1"
207
387
  },
208
388
  "devDependencies": {
209
389
  "@types/node": "^24.5.2",
@@ -238,7 +418,7 @@ MIT
238
418
  [security-url]: SECURITY.md
239
419
  [pnpm-badge]: https://img.shields.io/badge/pm-pnpm-ffd95a?logo=pnpm&logoColor=fff&labelColor=24292e
240
420
  [pnpm-url]: https://pnpm.io/
241
- [node-badge]: https://img.shields.io/badge/node-%3E%3D20-339933?logo=node.js&logoColor=fff
421
+ [node-badge]: https://img.shields.io/badge/node-%3E%3D22-339933?logo=node.js&logoColor=fff
242
422
  [node-url]: https://nodejs.org/
243
423
  [ts-badge]: https://img.shields.io/badge/TypeScript-5.x-3178C6?logo=typescript&logoColor=fff
244
424
  [ts-url]: https://www.typescriptlang.org/
package/SECURITY.md CHANGED
@@ -36,13 +36,14 @@ We provide security updates for the following versions:
36
36
 
37
37
  <!-- markdownlint-disable MD060 -->
38
38
 
39
- | Version | Supported | Security Updates | End of Life |
40
- | ------- | -------------- | ---------------- | ----------- |
41
- | 2.4.0+ | ✅ Current | Full support | TBD |
42
- | 2.3.x | ✅ Supported | Full support | TBD |
43
- | 2.0-2.2 | ⚠️ Limited | Critical only | 2024-12-31 |
44
- | 1.x.x | End of life | None | 2024-06-30 |
45
- | < 1.0 | ❌ End of life | None | 2024-01-01 |
39
+ | Version | Supported | Security Updates | End of Life |
40
+ | ------------ | -------------- | ---------------- | ----------- |
41
+ | 3.0.0-beta.1 | ✅ Current | Full support | TBD |
42
+ | 2.14.x | ✅ Supported | Full support | TBD |
43
+ | 2.8.0-2.13.x | Supported | Full support | 2025-06-30 |
44
+ | 2.0-2.7.x | ⚠️ Limited | Critical only | 2025-03-31 |
45
+ | 1.x.x | ❌ End of life | None | 2024-06-30 |
46
+ | < 1.0 | ❌ End of life | None | 2024-01-01 |
46
47
 
47
48
  **Update policy:**
48
49
 
@@ -83,15 +84,28 @@ We provide security updates for the following versions:
83
84
  ### Security Features
84
85
 
85
86
  - **Input validation**: Comprehensive path traversal prevention and sanitization via SecurityUtils
86
- - **Log injection protection**: All user inputs sanitized before logging
87
- - **Code injection prevention**: Strict validation for dynamic module loading
88
- - **TypeScript strict mode**: Compile-time safety checks
87
+ - CWE-22/23: Path traversal protection with URL-encoded sequence detection
88
+ - CWE-20: Proper input validation for CLI flags (--set, --env)
89
+ - Null byte injection prevention
90
+ - Reserved key validation in resource providers
91
+ - **Log injection protection**: All user inputs sanitized before logging (CWE-117)
92
+ - **Code injection prevention**: Strict validation for dynamic module loading (CWE-94)
93
+ - Regex escaping for dynamic pattern construction
94
+ - Function name validation before template generation
95
+ - **Command injection prevention**: Validated inputs for all shell commands (CWE-78/77/88)
96
+ - Release name validation (RFC 1123 subdomain)
97
+ - Namespace validation (Kubernetes naming rules)
98
+ - --set flag validation with support for nested paths and arrays
99
+ - **TypeScript strict mode**: Compile-time safety checks with all strict options enabled
89
100
  - **No eval()**: Static code generation only
90
101
  - **File system isolation**: Controlled output directory access with path validation
102
+ - Absolute path support with explicit allowAbsolute flag
103
+ - Base directory validation for all file operations
91
104
  - **Helm template validation**: Input validation for all template functions
92
105
  - **Karpenter security**: Secure node pool and scheduling configurations
93
106
  - **Performance optimization**: Efficient algorithms preventing DoS via resource exhaustion
94
- - **OWASP compliance**: Following secure coding guidelines (CWE-22, CWE-94, CWE-117)
107
+ - **OWASP compliance**: Following secure coding guidelines (CWE-22, CWE-23, CWE-94, CWE-117,
108
+ CWE-78, CWE-77, CWE-88, CWE-20)
95
109
 
96
110
  ## Security Considerations for Users
97
111
 
package/dist/cli.js CHANGED
@@ -142,8 +142,9 @@ async function cmdInit(name, silent = false) {
142
142
  if (!SecurityUtils.isValidChartName(validName)) {
143
143
  usageAndExit('Invalid chart name. Must be lowercase, start with a letter, and contain only letters, numbers, and dashes.');
144
144
  }
145
- const base = path.join(process.cwd(), validName);
146
- const chartFile = path.join(base, 'chart.ts');
145
+ const cwd = process.cwd();
146
+ const base = SecurityUtils.validatePath(path.join(cwd, validName), cwd);
147
+ const chartFile = SecurityUtils.validatePath(path.join(base, 'chart.ts'), cwd);
147
148
  fs.mkdirSync(base, { recursive: true });
148
149
  const { generateFlexibleSubchartTemplate } = await import('./lib/templates/flexible-subchart.js');
149
150
  fs.writeFileSync(chartFile, generateFlexibleSubchartTemplate(validName));
@@ -152,19 +153,31 @@ async function cmdInit(name, silent = false) {
152
153
  log(`Run 'tl synth ${validName}' to generate complete Helm chart`, silent);
153
154
  }
154
155
  async function cmdSynth(chartDirOrOutDir, flags, explicitOutDir) {
155
- let chartDir = process.cwd();
156
+ const cwd = process.cwd();
157
+ let chartDir = cwd;
156
158
  let outDir;
157
- if (chartDirOrOutDir && fs.existsSync(path.join(chartDirOrOutDir, 'chart.ts'))) {
158
- chartDir = path.resolve(chartDirOrOutDir);
159
- }
160
- else {
161
- outDir = chartDirOrOutDir;
159
+ if (chartDirOrOutDir) {
160
+ const resolvedPath = path.resolve(chartDirOrOutDir);
161
+ const validatedPath = SecurityUtils.validatePath(resolvedPath, cwd, { allowAbsolute: true });
162
+ const chartTsPath = SecurityUtils.validatePath(path.join(validatedPath, 'chart.ts'), cwd, {
163
+ allowAbsolute: true,
164
+ });
165
+ if (fs.existsSync(chartTsPath)) {
166
+ chartDir = validatedPath;
167
+ }
168
+ else {
169
+ outDir = chartDirOrOutDir;
170
+ }
162
171
  }
163
172
  if (explicitOutDir) {
164
173
  outDir = explicitOutDir;
165
174
  }
166
- const chartFile = path.join(chartDir, 'chart.ts');
167
- const defaultOutDir = path.join(chartDir, 'dist');
175
+ const chartFile = SecurityUtils.validatePath(path.join(chartDir, 'chart.ts'), cwd, {
176
+ allowAbsolute: true,
177
+ });
178
+ const defaultOutDir = SecurityUtils.validatePath(path.join(chartDir, 'dist'), cwd, {
179
+ allowAbsolute: true,
180
+ });
168
181
  if (!fs.existsSync(chartFile)) {
169
182
  console.error('chart.ts not found. Run `tl init` first.');
170
183
  process.exit(1);
@@ -177,7 +190,7 @@ async function cmdSynth(chartDirOrOutDir, flags, explicitOutDir) {
177
190
  if (modifiedContent === originalContent) {
178
191
  modifiedContent = originalContent.replace(/chart\.write\(['"][^'"]*['"]\)/, `chart.write('${safeOutDirLiteral}')`);
179
192
  }
180
- const tempChartFile = path.join(chartDir, '.timonel-temp-chart.ts');
193
+ const tempChartFile = SecurityUtils.validatePath(path.join(chartDir, '.timonel-temp-chart.ts'), cwd, { allowAbsolute: true });
181
194
  fs.writeFileSync(tempChartFile, modifiedContent);
182
195
  const wrapperScript = `
183
196
  import { pathToFileURL } from 'url';
@@ -237,11 +250,34 @@ async function cmdValidate(flags) {
237
250
  process.exit(result.status ?? 1);
238
251
  }
239
252
  }
253
+ function validateReleaseName(release) {
254
+ if (!/^[a-z0-9]([-a-z0-9]*[a-z0-9])?$/.test(release) || release.length > 53) {
255
+ console.error(`Invalid release name: ${SecurityUtils.sanitizeLogMessage(release)}`);
256
+ console.error('Release name must be lowercase alphanumeric with hyphens (max 53 chars)');
257
+ process.exit(1);
258
+ }
259
+ }
260
+ function validateNamespace(namespace) {
261
+ if (!/^[a-z0-9]([-a-z0-9]*[a-z0-9])?$/.test(namespace) || namespace.length > 63) {
262
+ console.error(`Invalid namespace: ${SecurityUtils.sanitizeLogMessage(namespace)}`);
263
+ console.error('Namespace must be lowercase alphanumeric with hyphens (max 63 chars)');
264
+ process.exit(1);
265
+ }
266
+ }
267
+ function validateSetFlag(setValue) {
268
+ if (!/^[a-zA-Z0-9._[\]-]+=.+$/.test(setValue)) {
269
+ console.error(`Invalid --set format: ${SecurityUtils.sanitizeLogMessage(setValue)}`);
270
+ console.error('Expected format: key=value');
271
+ process.exit(1);
272
+ }
273
+ }
240
274
  async function cmdDeploy(release, namespace, flags) {
241
275
  if (!release)
242
276
  usageAndExit('Missing <release>');
277
+ validateReleaseName(release);
243
278
  const args = ['upgrade', '--install', release, '.'];
244
279
  if (namespace) {
280
+ validateNamespace(namespace);
245
281
  args.push('--namespace', namespace);
246
282
  }
247
283
  if (flags?.env) {
@@ -256,6 +292,7 @@ async function cmdDeploy(release, namespace, flags) {
256
292
  }
257
293
  if (flags?.set) {
258
294
  for (const setValue of flags.set) {
295
+ validateSetFlag(setValue);
259
296
  args.push('--set', setValue);
260
297
  }
261
298
  }
@@ -465,7 +502,7 @@ async function cmdUmbrellaAdd(subchartPath, silent = false) {
465
502
  fs.mkdirSync(subchartDir, { recursive: true });
466
503
  const relativeSubchartPath = path.relative(chartsRoot, subchartDir) || subchartName;
467
504
  const normalizedSubchartPath = relativeSubchartPath.split(path.sep).join('/');
468
- const chartFile = path.join(subchartDir, 'chart.ts');
505
+ const chartFile = SecurityUtils.validatePath(path.join(subchartDir, 'chart.ts'), chartsRoot);
469
506
  const { generateFlexibleSubchartTemplate } = await import('./lib/templates/flexible-subchart.js');
470
507
  const subchartContent = generateFlexibleSubchartTemplate(subchartName);
471
508
  fs.writeFileSync(chartFile, subchartContent);
@@ -479,8 +516,9 @@ async function cmdUmbrellaAdd(subchartPath, silent = false) {
479
516
  log(`Subchart ${subchartName} added to umbrella at path '${normalizedSubchartPath}'`, silent);
480
517
  }
481
518
  async function cmdUmbrellaSynth(outDir, flags) {
482
- const umbrellaFile = path.join(process.cwd(), UMBRELLA_FILE_NAME);
483
- const defaultOutDir = path.join(process.cwd(), 'dist');
519
+ const cwd = process.cwd();
520
+ const umbrellaFile = SecurityUtils.validatePath(path.join(cwd, UMBRELLA_FILE_NAME), cwd);
521
+ const defaultOutDir = SecurityUtils.validatePath(path.join(cwd, 'dist'), cwd);
484
522
  if (!fs.existsSync(umbrellaFile)) {
485
523
  console.error('umbrella.ts not found. Run `tl umbrella init` first.');
486
524
  process.exit(1);
@@ -511,7 +549,8 @@ fs.mkdirSync(output, { recursive: true });
511
549
  await Promise.resolve(runner(output, synthOptions));
512
550
  console.log('Umbrella chart written to ' + output);
513
551
  `;
514
- const wrapperFile = path.join(process.cwd(), '.timonel-umbrella-wrapper.mjs');
552
+ const cwd = process.cwd();
553
+ const wrapperFile = SecurityUtils.validatePath(path.join(cwd, '.timonel-umbrella-wrapper.mjs'), cwd);
515
554
  try {
516
555
  fs.writeFileSync(wrapperFile, wrapperScript);
517
556
  const result = spawnSync(process.execPath, [TSX_CLI_PATH, wrapperFile], {
package/dist/index.d.ts CHANGED
@@ -12,5 +12,7 @@ export { DEFAULT_TERMINATION_GRACE_PERIOD, isValidDisruptionBudget, isValidKuber
12
12
  export { AWS_HELPERS, createHelper, FILE_ACCESS_HELPERS, formatHelpers, generateHelpersTemplate, getDefaultHelpers, getHelpersByCategory, KUBERNETES_HELPERS, SPRIG_HELPERS, STANDARD_HELPERS, TEMPLATE_FUNCTION_HELPERS, type HelperDefinition, } from './lib/utils/helmHelpers.js';
13
13
  export { createLogger, logger, LogLevel, TimonelLogger, type LogContext, type LoggerConfig, } from './lib/utils/logger.js';
14
14
  export { UmbrellaRutter } from './lib/umbrellaRutter.js';
15
- export { helmIf, helmRange, helmWith, helmInclude, helmDefine, helmVar, helmBlock, helmComment, helmFragment, createHelmExpression, isHelmConstruct, isHelmExpression, type HelmConstruct, type HelmExpression, type HelmContent, type HelmWhitespaceOptions, } from './lib/utils/helmControlStructures.js';
15
+ export { helmIf, helmIfSimple, helmRange, helmWith, helmInclude, helmDefine, helmVar, helmBlock, helmComment, helmFragment, createHelmExpression, isHelmConstruct, isHelmExpression, type HelmConstruct, type HelmExpression, type HelmContent, type HelmWhitespaceOptions, } from './lib/utils/helmControlStructures.js';
16
+ export { loadEnvVarsConfig, generateEnvVars, loadAndGenerateEnvVars, type EnvVarConfig, type LoadEnvVarsOptions, } from './lib/utils/envVarsLoader.js';
16
17
  export { dumpHelmAwareYaml, validateHelmYaml, type HelmValidationError, type HelmValidationResult, } from './lib/utils/helmYamlSerializer.js';
18
+ export { valuesRef, isHelmValue, isHelmCondition, isHelmFieldConditional, isHelmRange, isHelmWith, serializeHelmValue, serializeHelmCondition, type HelmValue, type HelmCondition, type HelmFieldConditional, type HelmRange, type HelmWith, type HelmHelpers, } from './lib/utils/valuesRef.js';
package/dist/index.js CHANGED
@@ -9,5 +9,7 @@ export { DEFAULT_TERMINATION_GRACE_PERIOD, isValidDisruptionBudget, isValidKuber
9
9
  export { AWS_HELPERS, createHelper, FILE_ACCESS_HELPERS, formatHelpers, generateHelpersTemplate, getDefaultHelpers, getHelpersByCategory, KUBERNETES_HELPERS, SPRIG_HELPERS, STANDARD_HELPERS, TEMPLATE_FUNCTION_HELPERS, } from './lib/utils/helmHelpers.js';
10
10
  export { createLogger, logger, LogLevel, TimonelLogger, } from './lib/utils/logger.js';
11
11
  export { UmbrellaRutter } from './lib/umbrellaRutter.js';
12
- export { helmIf, helmRange, helmWith, helmInclude, helmDefine, helmVar, helmBlock, helmComment, helmFragment, createHelmExpression, isHelmConstruct, isHelmExpression, } from './lib/utils/helmControlStructures.js';
12
+ export { helmIf, helmIfSimple, helmRange, helmWith, helmInclude, helmDefine, helmVar, helmBlock, helmComment, helmFragment, createHelmExpression, isHelmConstruct, isHelmExpression, } from './lib/utils/helmControlStructures.js';
13
+ export { loadEnvVarsConfig, generateEnvVars, loadAndGenerateEnvVars, } from './lib/utils/envVarsLoader.js';
13
14
  export { dumpHelmAwareYaml, validateHelmYaml, } from './lib/utils/helmYamlSerializer.js';
15
+ export { valuesRef, isHelmValue, isHelmCondition, isHelmFieldConditional, isHelmRange, isHelmWith, serializeHelmValue, serializeHelmCondition, } from './lib/utils/valuesRef.js';
package/dist/lib/helm.js CHANGED
@@ -36,6 +36,12 @@ export function indent(n, expr) {
36
36
  .join('\n');
37
37
  }
38
38
  export function template(name, context = '.') {
39
+ if (!isValidHelmPath(name)) {
40
+ throw new Error(`Invalid template name: ${name}`);
41
+ }
42
+ if (!isValidHelmPath(context)) {
43
+ throw new Error(`Invalid template context: ${context}`);
44
+ }
39
45
  return `{{ template "${name}" ${context} }}`;
40
46
  }
41
47
  export function include(name, context = '.') {
@@ -157,12 +163,33 @@ export function helmRange(collection, content, options = {}) {
157
163
  if (!collection.startsWith('.')) {
158
164
  throw new Error('Collection path must start with "." (e.g., ".Values.items")');
159
165
  }
166
+ if (!isValidHelmPath(collection)) {
167
+ throw new Error(`Invalid collection path: ${collection}`);
168
+ }
160
169
  const { keyValue = false, keyVar = '$key', valueVar = '$value', indexVar = '$index', itemVar = '$item', } = options;
170
+ const validateVarName = (varName, varType) => {
171
+ if (!varName.startsWith('$')) {
172
+ throw new Error(`${varType} must start with $ (e.g., $key, $value)`);
173
+ }
174
+ if (!/^\$[a-zA-Z_][a-zA-Z0-9_]*$/.test(varName)) {
175
+ throw new Error(`Invalid ${varType}: ${varName}`);
176
+ }
177
+ };
178
+ if (keyValue) {
179
+ validateVarName(keyVar, 'keyVar');
180
+ validateVarName(valueVar, 'valueVar');
181
+ }
182
+ if (options.indexVar !== undefined) {
183
+ validateVarName(indexVar, 'indexVar');
184
+ }
185
+ if (options.itemVar !== undefined) {
186
+ validateVarName(itemVar, 'itemVar');
187
+ }
161
188
  let rangeExpression;
162
189
  if (keyValue) {
163
190
  rangeExpression = `{{- range ${keyVar}, ${valueVar} := ${collection} }}`;
164
191
  }
165
- else if (options.indexVar && options.itemVar) {
192
+ else if (options.indexVar !== undefined && options.itemVar !== undefined) {
166
193
  rangeExpression = `{{- range ${indexVar}, ${itemVar} := ${collection} }}`;
167
194
  }
168
195
  else {
@@ -2,7 +2,7 @@ import * as fs from 'fs';
2
2
  import * as path from 'path';
3
3
  import { SecurityUtils } from './security.js';
4
4
  import { createLogger } from './utils/logger.js';
5
- import { dumpHelmAwareYaml } from './utils/helmYamlSerializer.js';
5
+ import { dumpHelmAwareYaml, postProcessFieldConditionals } from './utils/helmYamlSerializer.js';
6
6
  export class HelmChartWriter {
7
7
  static write(opts) {
8
8
  const { outDir, meta, defaultValues = {}, envValues = {}, assets, helpersTpl, notesTpl, valuesSchema, logger: customLogger, } = opts;
@@ -35,7 +35,8 @@ export class HelmChartWriter {
35
35
  timer();
36
36
  }
37
37
  static createDirectories(outDir) {
38
- fs.mkdirSync(path.join(outDir, 'templates'), { recursive: true });
38
+ const templatesDir = SecurityUtils.validatePath(path.join(outDir, 'templates'), outDir);
39
+ fs.mkdirSync(templatesDir, { recursive: true });
39
40
  }
40
41
  static writeChartYaml(outDir, meta) {
41
42
  const chartYaml = dumpHelmAwareYaml({
@@ -53,13 +54,16 @@ export class HelmChartWriter {
53
54
  icon: meta.icon,
54
55
  dependencies: meta.dependencies,
55
56
  });
56
- fs.writeFileSync(path.join(outDir, 'Chart.yaml'), chartYaml);
57
+ const chartYamlPath = SecurityUtils.validatePath(path.join(outDir, 'Chart.yaml'), outDir);
58
+ fs.writeFileSync(chartYamlPath, chartYaml);
57
59
  }
58
60
  static writeValuesFiles(outDir, defaultValues, envValues) {
59
- fs.writeFileSync(path.join(outDir, 'values.yaml'), dumpHelmAwareYaml(defaultValues));
61
+ const valuesPath = SecurityUtils.validatePath(path.join(outDir, 'values.yaml'), outDir);
62
+ fs.writeFileSync(valuesPath, dumpHelmAwareYaml(defaultValues));
60
63
  for (const [env, values] of Object.entries(envValues)) {
61
64
  const sanitizedEnv = SecurityUtils.sanitizeEnvironmentName(env);
62
- fs.writeFileSync(path.join(outDir, `values-${sanitizedEnv}.yaml`), dumpHelmAwareYaml(values));
65
+ const envValuesPath = SecurityUtils.validatePath(path.join(outDir, `values-${sanitizedEnv}.yaml`), outDir);
66
+ fs.writeFileSync(envValuesPath, dumpHelmAwareYaml(values));
63
67
  }
64
68
  }
65
69
  static writeAssets(outDir, assets) {
@@ -77,20 +81,23 @@ export class HelmChartWriter {
77
81
  .map((h) => [`{{- define "${h.name}" -}}`, h.template.trimEnd(), '{{- end }}', ''].join('\n'))
78
82
  .join('\n');
79
83
  }
80
- fs.writeFileSync(path.join(outDir, 'templates', '_helpers.tpl'), content);
84
+ const helpersPath = SecurityUtils.validatePath(path.join(outDir, 'templates', '_helpers.tpl'), outDir);
85
+ fs.writeFileSync(helpersPath, content);
81
86
  }
82
87
  static writeNotes(outDir, notesTpl) {
83
88
  if (!notesTpl)
84
89
  return;
85
- fs.writeFileSync(path.join(outDir, 'templates', 'NOTES.txt'), notesTpl.endsWith('\n') ? notesTpl : notesTpl + '\n');
90
+ const notesPath = SecurityUtils.validatePath(path.join(outDir, 'templates', 'NOTES.txt'), outDir);
91
+ fs.writeFileSync(notesPath, notesTpl.endsWith('\n') ? notesTpl : notesTpl + '\n');
86
92
  }
87
93
  static writeSchema(outDir, valuesSchema) {
88
94
  if (!valuesSchema)
89
95
  return;
90
- fs.writeFileSync(path.join(outDir, 'values.schema.json'), JSON.stringify(valuesSchema, null, 2) + '\n');
96
+ const schemaPath = SecurityUtils.validatePath(path.join(outDir, 'values.schema.json'), outDir);
97
+ fs.writeFileSync(schemaPath, JSON.stringify(valuesSchema, null, 2) + '\n');
91
98
  }
92
99
  static writeHelmIgnore(outDir) {
93
- const helmIgnorePath = path.join(outDir, '.helmignore');
100
+ const helmIgnorePath = SecurityUtils.validatePath(path.join(outDir, '.helmignore'), outDir);
94
101
  if (!fs.existsSync(helmIgnorePath)) {
95
102
  const helmIgnore = [
96
103
  '# VCS',
@@ -149,19 +156,33 @@ function writeSingleAssetFile(outDir, targetDir, directorySegments, fileBaseName
149
156
  const chartSubdir = path.join(outDir, targetDir, ...directorySegments);
150
157
  SecurityUtils.validatePath(chartSubdir, outDir);
151
158
  fs.mkdirSync(chartSubdir, { recursive: true });
159
+ if (fileBaseName.includes('..') || fileBaseName.includes('/') || fileBaseName.includes('\\')) {
160
+ throw new Error(`Invalid fileBaseName: ${fileBaseName}`);
161
+ }
152
162
  const filename = `${fileBaseName}.yaml`;
163
+ if (filename.includes('..') || filename.includes('/') || filename.includes('\\')) {
164
+ throw new Error(`Invalid filename: ${filename}`);
165
+ }
153
166
  const absolutePath = path.join(chartSubdir, filename);
154
167
  SecurityUtils.validatePath(absolutePath, outDir);
155
- fs.writeFileSync(absolutePath, yaml.endsWith('\n') ? yaml : `${yaml}\n`);
168
+ const processedYaml = postProcessFieldConditionals(yaml);
169
+ fs.writeFileSync(absolutePath, processedYaml.endsWith('\n') ? processedYaml : `${processedYaml}\n`);
156
170
  }
157
171
  function writeMultipleAssetFiles(outDir, targetDir, directorySegments, fileBaseName, yaml) {
158
172
  const chartSubdir = path.join(outDir, targetDir, ...directorySegments);
159
173
  SecurityUtils.validatePath(chartSubdir, outDir);
160
174
  fs.mkdirSync(chartSubdir, { recursive: true });
161
- const parts = splitDocs(yaml);
175
+ const processedYaml = postProcessFieldConditionals(yaml);
176
+ const parts = splitDocs(processedYaml);
162
177
  parts.forEach((doc, index) => {
178
+ if (fileBaseName.includes('..') || fileBaseName.includes('/') || fileBaseName.includes('\\')) {
179
+ throw new Error(`Invalid fileBaseName: ${fileBaseName}`);
180
+ }
163
181
  const suffix = parts.length > 1 ? `-${index + 1}` : '';
164
182
  const filename = `${fileBaseName}${suffix}.yaml`;
183
+ if (filename.includes('..') || filename.includes('/') || filename.includes('\\')) {
184
+ throw new Error(`Invalid filename: ${filename}`);
185
+ }
165
186
  const absolutePath = path.join(chartSubdir, filename);
166
187
  SecurityUtils.validatePath(absolutePath, outDir);
167
188
  fs.writeFileSync(absolutePath, doc.endsWith('\n') ? doc : `${doc}\n`);
@@ -48,6 +48,11 @@ export class BaseResourceProvider {
48
48
  createRootLevelApiObject(name, apiVersion, kind, fields, labels, annotations) {
49
49
  this.validateKubernetesName(name, kind);
50
50
  this.validateLabels(labels, kind);
51
+ const reservedKeys = ['apiVersion', 'kind', 'metadata'];
52
+ const conflictingKeys = Object.keys(fields).filter((key) => reservedKeys.includes(key));
53
+ if (conflictingKeys.length > 0) {
54
+ throw new Error(`Fields object contains reserved keys: ${conflictingKeys.join(', ')}. These keys cannot be overridden.`);
55
+ }
51
56
  return new ApiObject(this.chart, name, {
52
57
  apiVersion,
53
58
  kind,
@@ -120,7 +120,8 @@ export class AWSResources extends BaseResourceProvider {
120
120
  throw new Error(`Ingress path "${path}" must start with /`);
121
121
  }
122
122
  if (normalizedPath.includes('../') || normalizedPath.includes('./')) {
123
- console.warn(`Warning: Ingress path "${path}" contains path traversal-like sequences`);
123
+ const sanitizedPath = path.replace(/[\r\n]/g, '');
124
+ console.warn(`Warning: Ingress path "${sanitizedPath}" contains path traversal-like sequences`);
124
125
  }
125
126
  }
126
127
  validatePathType(pathType) {