@theunwalked/cardigantime 0.0.18 → 0.0.20

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
@@ -138,6 +138,9 @@ Merges configuration from multiple sources in order of precedence:
138
138
  2. **Configuration file(s)** (medium priority)
139
139
  3. **Default values** (lowest priority)
140
140
 
141
+ ### Flexible YAML Support
142
+ Supports both `.yaml` and `.yml` file extensions with automatic fallback - if `config.yaml` isn't found, Cardigantime automatically tries `config.yml` (and vice versa).
143
+
141
144
  ### Hierarchical Configuration Discovery
142
145
  Supports hierarchical configuration discovery, similar to how `.gitignore`, `.eslintrc`, or `package.json` work - searching up the directory tree for configuration directories.
143
146
 
@@ -446,10 +446,21 @@ const create$1 = (params)=>{
446
446
  }
447
447
  /**
448
448
  * Sets a nested value in an object using dot notation.
449
- */ function setNestedValue$1(obj, path, value) {
449
+ */ function isUnsafeKey$1(key) {
450
+ return key === '__proto__' || key === 'constructor' || key === 'prototype';
451
+ }
452
+ function setNestedValue$1(obj, path, value) {
450
453
  const keys = path.split('.');
451
454
  const lastKey = keys.pop();
455
+ // Prevent prototype pollution via special property names
456
+ if (isUnsafeKey$1(lastKey) || keys.some(isUnsafeKey$1)) {
457
+ return;
458
+ }
452
459
  const target = keys.reduce((current, key)=>{
460
+ // Skip if this is an unsafe key (already checked above, but defensive)
461
+ if (isUnsafeKey$1(key)) {
462
+ return current;
463
+ }
453
464
  if (!(key in current)) {
454
465
  current[key] = {};
455
466
  }
@@ -547,6 +558,43 @@ const create$1 = (params)=>{
547
558
  logger === null || logger === void 0 ? void 0 : logger.verbose(`Discovery complete. Found ${discoveredDirs.length} config directories`);
548
559
  return discoveredDirs;
549
560
  }
561
+ /**
562
+ * Tries to find a config file with alternative extensions (.yaml or .yml).
563
+ *
564
+ * @param storage Storage instance to use for file operations
565
+ * @param configDir The directory containing the config file
566
+ * @param configFileName The base config file name (may have .yaml or .yml extension)
567
+ * @param logger Optional logger for debugging
568
+ * @returns Promise resolving to the found config file path or null if not found
569
+ */ async function findConfigFileWithExtension$1(storage, configDir, configFileName, logger) {
570
+ const configFilePath = path.join(configDir, configFileName);
571
+ // First try the exact filename as specified
572
+ const exists = await storage.exists(configFilePath);
573
+ if (exists) {
574
+ const isReadable = await storage.isFileReadable(configFilePath);
575
+ if (isReadable) {
576
+ return configFilePath;
577
+ }
578
+ }
579
+ // If the exact filename doesn't exist or isn't readable, try alternative extensions
580
+ // Only do this if the filename has a .yaml or .yml extension
581
+ const ext = path.extname(configFileName);
582
+ if (ext === '.yaml' || ext === '.yml') {
583
+ const baseName = path.basename(configFileName, ext);
584
+ const alternativeExt = ext === '.yaml' ? '.yml' : '.yaml';
585
+ const alternativePath = path.join(configDir, baseName + alternativeExt);
586
+ logger === null || logger === void 0 ? void 0 : logger.debug(`Config file not found at ${configFilePath}, trying alternative: ${alternativePath}`);
587
+ const altExists = await storage.exists(alternativePath);
588
+ if (altExists) {
589
+ const altIsReadable = await storage.isFileReadable(alternativePath);
590
+ if (altIsReadable) {
591
+ logger === null || logger === void 0 ? void 0 : logger.debug(`Found config file with alternative extension: ${alternativePath}`);
592
+ return alternativePath;
593
+ }
594
+ }
595
+ }
596
+ return null;
597
+ }
550
598
  /**
551
599
  * Loads and parses a configuration file from a directory.
552
600
  *
@@ -561,17 +609,12 @@ const create$1 = (params)=>{
561
609
  const storage = create$1({
562
610
  log: (logger === null || logger === void 0 ? void 0 : logger.debug) || (()=>{})
563
611
  });
564
- const configFilePath = path.join(configDir, configFileName);
565
612
  try {
566
- logger === null || logger === void 0 ? void 0 : logger.verbose(`Attempting to load config file: ${configFilePath}`);
567
- const exists = await storage.exists(configFilePath);
568
- if (!exists) {
569
- logger === null || logger === void 0 ? void 0 : logger.debug(`Config file does not exist: ${configFilePath}`);
570
- return null;
571
- }
572
- const isReadable = await storage.isFileReadable(configFilePath);
573
- if (!isReadable) {
574
- logger === null || logger === void 0 ? void 0 : logger.debug(`Config file exists but is not readable: ${configFilePath}`);
613
+ logger === null || logger === void 0 ? void 0 : logger.verbose(`Attempting to load config file: ${path.join(configDir, configFileName)}`);
614
+ // Try to find the config file with alternative extensions
615
+ const configFilePath = await findConfigFileWithExtension$1(storage, configDir, configFileName, logger);
616
+ if (!configFilePath) {
617
+ logger === null || logger === void 0 ? void 0 : logger.debug(`Config file does not exist: ${path.join(configDir, configFileName)}`);
575
618
  return null;
576
619
  }
577
620
  const yamlContent = await storage.readFile(configFilePath, encoding);
@@ -589,7 +632,7 @@ const create$1 = (params)=>{
589
632
  return null;
590
633
  }
591
634
  } catch (error) {
592
- logger === null || logger === void 0 ? void 0 : logger.debug(`Error loading config from ${configFilePath}: ${error.message}`);
635
+ logger === null || logger === void 0 ? void 0 : logger.debug(`Error loading config from ${path.join(configDir, configFileName)}: ${error.message}`);
593
636
  return null;
594
637
  }
595
638
  }
@@ -846,12 +889,26 @@ const create$1 = (params)=>{
846
889
  */ function getNestedValue(obj, path) {
847
890
  return path.split('.').reduce((current, key)=>current === null || current === void 0 ? void 0 : current[key], obj);
848
891
  }
892
+ /**
893
+ * Checks if a key is unsafe for prototype pollution prevention.
894
+ */ function isUnsafeKey(key) {
895
+ return key === '__proto__' || key === 'constructor' || key === 'prototype';
896
+ }
849
897
  /**
850
898
  * Sets a nested value in an object using dot notation.
899
+ * Prevents prototype pollution by rejecting dangerous property names.
851
900
  */ function setNestedValue(obj, path, value) {
852
901
  const keys = path.split('.');
853
902
  const lastKey = keys.pop();
903
+ // Prevent prototype pollution via special property names
904
+ if (isUnsafeKey(lastKey) || keys.some(isUnsafeKey)) {
905
+ return;
906
+ }
854
907
  const target = keys.reduce((current, key)=>{
908
+ // Skip if this is an unsafe key (already checked above, but defensive)
909
+ if (isUnsafeKey(key)) {
910
+ return current;
911
+ }
855
912
  if (!(key in current)) {
856
913
  current[key] = {};
857
914
  }
@@ -1060,6 +1117,45 @@ const create$1 = (params)=>{
1060
1117
  });
1061
1118
  return config;
1062
1119
  };
1120
+ /**
1121
+ * Tries to find a config file with alternative extensions (.yaml or .yml).
1122
+ *
1123
+ * @param storage Storage instance to use for file operations
1124
+ * @param configDir The directory containing the config file
1125
+ * @param configFileName The base config file name (may have .yaml or .yml extension)
1126
+ * @param logger Logger for debugging
1127
+ * @returns Promise resolving to the found config file path or null if not found
1128
+ */ async function findConfigFileWithExtension(storage, configDir, configFileName, logger) {
1129
+ // Validate the config file name to prevent path traversal
1130
+ const configFilePath = validatePath(configFileName, configDir);
1131
+ // First try the exact filename as specified
1132
+ const exists = await storage.exists(configFilePath);
1133
+ if (exists) {
1134
+ const isReadable = await storage.isFileReadable(configFilePath);
1135
+ if (isReadable) {
1136
+ return configFilePath;
1137
+ }
1138
+ }
1139
+ // If the exact filename doesn't exist or isn't readable, try alternative extensions
1140
+ // Only do this if the filename has a .yaml or .yml extension
1141
+ const ext = path__namespace.extname(configFileName);
1142
+ if (ext === '.yaml' || ext === '.yml') {
1143
+ const baseName = path__namespace.basename(configFileName, ext);
1144
+ const alternativeExt = ext === '.yaml' ? '.yml' : '.yaml';
1145
+ const alternativeFileName = baseName + alternativeExt;
1146
+ const alternativePath = validatePath(alternativeFileName, configDir);
1147
+ logger.debug(`Config file not found at ${configFilePath}, trying alternative: ${alternativePath}`);
1148
+ const altExists = await storage.exists(alternativePath);
1149
+ if (altExists) {
1150
+ const altIsReadable = await storage.isFileReadable(alternativePath);
1151
+ if (altIsReadable) {
1152
+ logger.debug(`Found config file with alternative extension: ${alternativePath}`);
1153
+ return alternativePath;
1154
+ }
1155
+ }
1156
+ }
1157
+ return null;
1158
+ }
1063
1159
  /**
1064
1160
  * Loads configuration from a single directory (traditional mode).
1065
1161
  *
@@ -1071,11 +1167,16 @@ const create$1 = (params)=>{
1071
1167
  const storage = create$1({
1072
1168
  log: logger.debug
1073
1169
  });
1074
- const configFile = validatePath(options.defaults.configFile, resolvedConfigDir);
1075
1170
  logger.verbose('Attempting to load config file for cardigantime');
1076
1171
  let rawFileConfig = {};
1077
1172
  try {
1078
- const yamlContent = await storage.readFile(configFile, options.defaults.encoding);
1173
+ // Try to find the config file with alternative extensions
1174
+ const configFilePath = await findConfigFileWithExtension(storage, resolvedConfigDir, options.defaults.configFile, logger);
1175
+ if (!configFilePath) {
1176
+ logger.verbose('Configuration file not found. Using empty configuration.');
1177
+ return rawFileConfig;
1178
+ }
1179
+ const yamlContent = await storage.readFile(configFilePath, options.defaults.encoding);
1079
1180
  // SECURITY FIX: Use safer parsing options to prevent code execution vulnerabilities
1080
1181
  const parsedYaml = yaml__namespace.load(yamlContent);
1081
1182
  if (parsedYaml !== null && typeof parsedYaml === 'object') {
@@ -1085,6 +1186,10 @@ const create$1 = (params)=>{
1085
1186
  logger.warn('Ignoring invalid configuration format. Expected an object, got ' + typeof parsedYaml);
1086
1187
  }
1087
1188
  } catch (error) {
1189
+ // Re-throw security-related errors (path validation failures)
1190
+ if (error.message && /Invalid path|path traversal|absolute path/i.test(error.message)) {
1191
+ throw error;
1192
+ }
1088
1193
  if (error.code === 'ENOENT' || /not found|no such file/i.test(error.message)) {
1089
1194
  logger.verbose('Configuration file not found. Using empty configuration.');
1090
1195
  } else {