@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 +3 -0
- package/dist/cardigantime.cjs +119 -14
- package/dist/cardigantime.cjs.map +1 -1
- package/dist/read.js +64 -2
- package/dist/read.js.map +1 -1
- package/dist/util/hierarchical.js +55 -12
- package/dist/util/hierarchical.js.map +1 -1
- package/package.json +2 -2
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
|
|
package/dist/cardigantime.cjs
CHANGED
|
@@ -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
|
|
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: ${
|
|
567
|
-
|
|
568
|
-
|
|
569
|
-
|
|
570
|
-
|
|
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 ${
|
|
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
|
-
|
|
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 {
|