@theunwalked/cardigantime 0.0.1 → 0.0.3

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 (52) hide show
  1. package/README.md +699 -0
  2. package/dist/cardigantime.cjs +907 -15
  3. package/dist/cardigantime.cjs.map +1 -1
  4. package/dist/cardigantime.d.ts +42 -0
  5. package/dist/cardigantime.js +49 -345
  6. package/dist/cardigantime.js.map +1 -1
  7. package/dist/configure.d.ts +50 -1
  8. package/dist/configure.js +102 -3
  9. package/dist/configure.js.map +1 -1
  10. package/dist/constants.d.ts +17 -0
  11. package/dist/constants.js +17 -9
  12. package/dist/constants.js.map +1 -1
  13. package/dist/error/ArgumentError.d.ts +26 -0
  14. package/dist/error/ArgumentError.js +48 -0
  15. package/dist/error/ArgumentError.js.map +1 -0
  16. package/dist/error/ConfigurationError.d.ts +21 -0
  17. package/dist/error/ConfigurationError.js +46 -0
  18. package/dist/error/ConfigurationError.js.map +1 -0
  19. package/dist/error/FileSystemError.d.ts +30 -0
  20. package/dist/error/FileSystemError.js +58 -0
  21. package/dist/error/FileSystemError.js.map +1 -0
  22. package/dist/error/index.d.ts +3 -0
  23. package/dist/read.d.ts +30 -0
  24. package/dist/read.js +105 -12
  25. package/dist/read.js.map +1 -1
  26. package/dist/types.d.ts +63 -0
  27. package/dist/types.js +5 -3
  28. package/dist/types.js.map +1 -1
  29. package/dist/util/storage.js +33 -4
  30. package/dist/util/storage.js.map +1 -1
  31. package/dist/validate.d.ts +96 -1
  32. package/dist/validate.js +164 -20
  33. package/dist/validate.js.map +1 -1
  34. package/package.json +30 -23
  35. package/.gitcarve/config.yaml +0 -10
  36. package/.gitcarve/context/content.md +0 -1
  37. package/dist/configure.cjs +0 -12
  38. package/dist/configure.cjs.map +0 -1
  39. package/dist/constants.cjs +0 -35
  40. package/dist/constants.cjs.map +0 -1
  41. package/dist/read.cjs +0 -69
  42. package/dist/read.cjs.map +0 -1
  43. package/dist/types.cjs +0 -13
  44. package/dist/types.cjs.map +0 -1
  45. package/dist/util/storage.cjs +0 -149
  46. package/dist/util/storage.cjs.map +0 -1
  47. package/dist/validate.cjs +0 -130
  48. package/dist/validate.cjs.map +0 -1
  49. package/eslint.config.mjs +0 -82
  50. package/nodemon.json +0 -14
  51. package/vite.config.ts +0 -98
  52. package/vitest.config.ts +0 -17
@@ -0,0 +1 @@
1
+ {"version":3,"file":"FileSystemError.js","sources":["../../src/error/FileSystemError.ts"],"sourcesContent":["/**\n * Error thrown when file system operations fail\n */\nexport class FileSystemError extends Error {\n public readonly errorType: 'not_found' | 'not_readable' | 'not_writable' | 'creation_failed' | 'operation_failed';\n public readonly path: string;\n public readonly operation: string;\n public readonly originalError?: Error;\n\n constructor(\n errorType: 'not_found' | 'not_readable' | 'not_writable' | 'creation_failed' | 'operation_failed',\n message: string,\n path: string,\n operation: string,\n originalError?: Error\n ) {\n super(message);\n this.name = 'FileSystemError';\n this.errorType = errorType;\n this.path = path;\n this.operation = operation;\n this.originalError = originalError;\n }\n\n /**\n * Creates an error for when a required directory doesn't exist\n */\n static directoryNotFound(path: string, isRequired: boolean = false): FileSystemError {\n const message = isRequired\n ? 'Configuration directory does not exist and is required'\n : 'Configuration directory not found';\n return new FileSystemError('not_found', message, path, 'directory_access');\n }\n\n /**\n * Creates an error for when a directory exists but isn't readable\n */\n static directoryNotReadable(path: string): FileSystemError {\n const message = 'Configuration directory exists but is not readable';\n return new FileSystemError('not_readable', message, path, 'directory_read');\n }\n\n /**\n * Creates an error for directory creation failures\n */\n static directoryCreationFailed(path: string, originalError: Error): FileSystemError {\n const message = 'Failed to create directory: ' + (originalError.message || 'Unknown error');\n return new FileSystemError('creation_failed', message, path, 'directory_create', originalError);\n }\n\n /**\n * Creates an error for file operation failures (glob, etc.)\n */\n static operationFailed(operation: string, path: string, originalError: Error): FileSystemError {\n const message = `Failed to ${operation}: ${originalError.message || 'Unknown error'}`;\n return new FileSystemError('operation_failed', message, path, operation, originalError);\n }\n\n /**\n * Creates an error for when a file is not found\n */\n static fileNotFound(path: string): FileSystemError {\n const message = 'Configuration file not found';\n return new FileSystemError('not_found', message, path, 'file_read');\n }\n} "],"names":["FileSystemError","Error","directoryNotFound","path","isRequired","message","directoryNotReadable","directoryCreationFailed","originalError","operationFailed","operation","fileNotFound","errorType","name"],"mappings":"AAAA;;AAEC,IAAA,SAAA,gBAAA,CAAA,GAAA,EAAA,GAAA,EAAA,KAAA,EAAA;;;;;;;;;;;;;AACM,MAAMA,eAAAA,SAAwBC,KAAAA,CAAAA;AAqBjC;;AAEC,QACD,OAAOC,iBAAAA,CAAkBC,IAAY,EAAEC,UAAAA,GAAsB,KAAK,EAAmB;QACjF,MAAMC,OAAAA,GAAUD,aACV,wDAAA,GACA,mCAAA;AACN,QAAA,OAAO,IAAIJ,eAAAA,CAAgB,WAAA,EAAaK,OAAAA,EAASF,IAAAA,EAAM,kBAAA,CAAA;AAC3D;AAEA;;QAGA,OAAOG,oBAAAA,CAAqBH,IAAY,EAAmB;AACvD,QAAA,MAAME,OAAAA,GAAU,oDAAA;AAChB,QAAA,OAAO,IAAIL,eAAAA,CAAgB,cAAA,EAAgBK,OAAAA,EAASF,IAAAA,EAAM,gBAAA,CAAA;AAC9D;AAEA;;AAEC,QACD,OAAOI,uBAAAA,CAAwBJ,IAAY,EAAEK,aAAoB,EAAmB;AAChF,QAAA,MAAMH,UAAU,8BAAA,IAAkCG,aAAAA,CAAcH,OAAO,IAAI,eAAc,CAAA;AACzF,QAAA,OAAO,IAAIL,eAAAA,CAAgB,iBAAA,EAAmBK,OAAAA,EAASF,MAAM,kBAAA,EAAoBK,aAAAA,CAAAA;AACrF;AAEA;;AAEC,QACD,OAAOC,eAAAA,CAAgBC,SAAiB,EAAEP,IAAY,EAAEK,aAAoB,EAAmB;QAC3F,MAAMH,OAAAA,GAAU,CAAC,UAAU,EAAEK,SAAAA,CAAU,EAAE,EAAEF,aAAAA,CAAcH,OAAO,IAAI,eAAA,CAAA,CAAiB;AACrF,QAAA,OAAO,IAAIL,eAAAA,CAAgB,kBAAA,EAAoBK,OAAAA,EAASF,MAAMO,SAAAA,EAAWF,aAAAA,CAAAA;AAC7E;AAEA;;QAGA,OAAOG,YAAAA,CAAaR,IAAY,EAAmB;AAC/C,QAAA,MAAME,OAAAA,GAAU,8BAAA;AAChB,QAAA,OAAO,IAAIL,eAAAA,CAAgB,WAAA,EAAaK,OAAAA,EAASF,IAAAA,EAAM,WAAA,CAAA;AAC3D;IAvDA,WAAA,CACIS,SAAiG,EACjGP,OAAe,EACfF,IAAY,EACZO,SAAiB,EACjBF,aAAqB,CACvB;AACE,QAAA,KAAK,CAACH,OAAAA,CAAAA,EAZV,gBAAA,CAAA,IAAA,EAAgBO,WAAAA,EAAhB,SACA,gBAAA,CAAA,IAAA,EAAgBT,MAAAA,EAAhB,MAAA,CAAA,EACA,uBAAgBO,WAAAA,EAAhB,MAAA,CAAA,EACA,gBAAA,CAAA,IAAA,EAAgBF,iBAAhB,MAAA,CAAA;QAUI,IAAI,CAACK,IAAI,GAAG,iBAAA;QACZ,IAAI,CAACD,SAAS,GAAGA,SAAAA;QACjB,IAAI,CAACT,IAAI,GAAGA,IAAAA;QACZ,IAAI,CAACO,SAAS,GAAGA,SAAAA;QACjB,IAAI,CAACF,aAAa,GAAGA,aAAAA;AACzB;AA2CJ;;;;"}
@@ -0,0 +1,3 @@
1
+ export { ArgumentError } from './ArgumentError';
2
+ export { ConfigurationError } from './ConfigurationError';
3
+ export { FileSystemError } from './FileSystemError';
package/dist/read.d.ts CHANGED
@@ -1,3 +1,33 @@
1
1
  import { z, ZodObject } from 'zod';
2
2
  import { Args, ConfigSchema, Options } from './types';
3
+ /**
4
+ * Reads configuration from files and merges it with CLI arguments.
5
+ *
6
+ * This function implements the core configuration loading logic:
7
+ * 1. Validates and resolves the configuration directory path
8
+ * 2. Attempts to read the YAML configuration file
9
+ * 3. Safely parses the YAML content with security protections
10
+ * 4. Merges file configuration with runtime arguments
11
+ * 5. Returns a typed configuration object
12
+ *
13
+ * The function handles missing files gracefully and provides detailed
14
+ * logging for troubleshooting configuration issues.
15
+ *
16
+ * @template T - The Zod schema shape type for configuration validation
17
+ * @param args - Parsed command-line arguments containing potential config overrides
18
+ * @param options - Cardigantime options with defaults, schema, and logger
19
+ * @returns Promise resolving to the merged and typed configuration object
20
+ * @throws {Error} When configuration directory is invalid or required files cannot be read
21
+ *
22
+ * @example
23
+ * ```typescript
24
+ * const config = await read(cliArgs, {
25
+ * defaults: { configDirectory: './config', configFile: 'app.yaml' },
26
+ * configShape: MySchema.shape,
27
+ * logger: console,
28
+ * features: ['config']
29
+ * });
30
+ * // config is fully typed based on your schema
31
+ * ```
32
+ */
3
33
  export declare const read: <T extends z.ZodRawShape>(args: Args, options: Options<T>) => Promise<z.infer<ZodObject<T & typeof ConfigSchema.shape>>>;
package/dist/read.js CHANGED
@@ -1,36 +1,129 @@
1
1
  import * as yaml from 'js-yaml';
2
- import path from 'path';
2
+ import * as path from 'path';
3
3
  import { create } from './util/storage.js';
4
4
 
5
- function clean(obj) {
5
+ /**
6
+ * Removes undefined values from an object to create a clean configuration.
7
+ * This is used to merge configuration sources while avoiding undefined pollution.
8
+ *
9
+ * @param obj - The object to clean
10
+ * @returns A new object with undefined values filtered out
11
+ */ function clean(obj) {
6
12
  return Object.fromEntries(Object.entries(obj).filter(([_, v])=>v !== undefined));
7
13
  }
8
- const read = async (args, options)=>{
14
+ /**
15
+ * Validates and secures a user-provided path to prevent path traversal attacks.
16
+ *
17
+ * Security checks include:
18
+ * - Path traversal prevention (blocks '..')
19
+ * - Absolute path detection
20
+ * - Path separator validation
21
+ *
22
+ * @param userPath - The user-provided path component
23
+ * @param basePath - The base directory to join the path with
24
+ * @returns The safely joined and normalized path
25
+ * @throws {Error} When path traversal or absolute paths are detected
26
+ */ function validatePath(userPath, basePath) {
27
+ if (!userPath || !basePath) {
28
+ throw new Error('Invalid path parameters');
29
+ }
30
+ const normalized = path.normalize(userPath);
31
+ // Prevent path traversal attacks
32
+ if (normalized.includes('..') || path.isAbsolute(normalized)) {
33
+ throw new Error('Invalid path: path traversal detected');
34
+ }
35
+ // Ensure the path doesn't start with a path separator
36
+ if (normalized.startsWith('/') || normalized.startsWith('\\')) {
37
+ throw new Error('Invalid path: absolute path detected');
38
+ }
39
+ return path.join(basePath, normalized);
40
+ }
41
+ /**
42
+ * Validates a configuration directory path for security and basic formatting.
43
+ *
44
+ * Performs validation to prevent:
45
+ * - Null byte injection attacks
46
+ * - Extremely long paths that could cause DoS
47
+ * - Empty or invalid directory specifications
48
+ *
49
+ * @param configDir - The configuration directory path to validate
50
+ * @returns The normalized configuration directory path
51
+ * @throws {Error} When the directory path is invalid or potentially dangerous
52
+ */ function validateConfigDirectory(configDir) {
53
+ if (!configDir) {
54
+ throw new Error('Configuration directory is required');
55
+ }
56
+ // Check for null bytes which could be used for path injection
57
+ if (configDir.includes('\0')) {
58
+ throw new Error('Invalid path: null byte detected');
59
+ }
60
+ const normalized = path.normalize(configDir);
61
+ // Basic validation - could be expanded based on requirements
62
+ if (normalized.length > 1000) {
63
+ throw new Error('Configuration directory path too long');
64
+ }
65
+ return normalized;
66
+ }
67
+ /**
68
+ * Reads configuration from files and merges it with CLI arguments.
69
+ *
70
+ * This function implements the core configuration loading logic:
71
+ * 1. Validates and resolves the configuration directory path
72
+ * 2. Attempts to read the YAML configuration file
73
+ * 3. Safely parses the YAML content with security protections
74
+ * 4. Merges file configuration with runtime arguments
75
+ * 5. Returns a typed configuration object
76
+ *
77
+ * The function handles missing files gracefully and provides detailed
78
+ * logging for troubleshooting configuration issues.
79
+ *
80
+ * @template T - The Zod schema shape type for configuration validation
81
+ * @param args - Parsed command-line arguments containing potential config overrides
82
+ * @param options - Cardigantime options with defaults, schema, and logger
83
+ * @returns Promise resolving to the merged and typed configuration object
84
+ * @throws {Error} When configuration directory is invalid or required files cannot be read
85
+ *
86
+ * @example
87
+ * ```typescript
88
+ * const config = await read(cliArgs, {
89
+ * defaults: { configDirectory: './config', configFile: 'app.yaml' },
90
+ * configShape: MySchema.shape,
91
+ * logger: console,
92
+ * features: ['config']
93
+ * });
94
+ * // config is fully typed based on your schema
95
+ * ```
96
+ */ const read = async (args, options)=>{
9
97
  var _options_defaults;
10
98
  const logger = options.logger;
11
99
  const storage = create({
12
100
  log: logger.debug
13
101
  });
14
- const resolvedConfigDir = args.configDirectory || ((_options_defaults = options.defaults) === null || _options_defaults === void 0 ? void 0 : _options_defaults.configDirectory);
15
- logger.debug(`Resolved config directory: ${resolvedConfigDir}`);
16
- const configFile = path.join(resolvedConfigDir, options.defaults.configFile);
17
- logger.debug(`Attempting to load config file for cardigantime: ${configFile}`);
102
+ const rawConfigDir = args.configDirectory || ((_options_defaults = options.defaults) === null || _options_defaults === void 0 ? void 0 : _options_defaults.configDirectory);
103
+ if (!rawConfigDir) {
104
+ throw new Error('Configuration directory must be specified');
105
+ }
106
+ const resolvedConfigDir = validateConfigDirectory(rawConfigDir);
107
+ logger.debug('Resolved config directory');
108
+ const configFile = validatePath(options.defaults.configFile, resolvedConfigDir);
109
+ logger.debug('Attempting to load config file for cardigantime');
18
110
  let rawFileConfig = {};
19
111
  try {
20
112
  const yamlContent = await storage.readFile(configFile, options.defaults.encoding);
113
+ // SECURITY FIX: Use safer parsing options to prevent code execution vulnerabilities
21
114
  const parsedYaml = yaml.load(yamlContent);
22
115
  if (parsedYaml !== null && typeof parsedYaml === 'object') {
23
116
  rawFileConfig = parsedYaml;
24
- logger.debug('Loaded Raw File Config for getValuesFromFile: \n\n%s\n\n', JSON.stringify(rawFileConfig, null, 2));
117
+ logger.debug('Loaded configuration file successfully');
25
118
  } else if (parsedYaml !== null) {
26
- logger.warn(`Ignoring invalid configuration format in ${configFile} for cardigantime. Expected an object, got ${typeof parsedYaml}.`);
119
+ logger.warn('Ignoring invalid configuration format. Expected an object, got ' + typeof parsedYaml);
27
120
  }
28
121
  } catch (error) {
29
122
  if (error.code === 'ENOENT' || /not found|no such file/i.test(error.message)) {
30
- logger.debug(`Configuration file not found at ${configFile} for cardigantime. Returning empty object.`);
123
+ logger.debug('Configuration file not found. Using empty configuration.');
31
124
  } else {
32
- // Log error but don't throw, just return empty object as per the goal of just *getting* values
33
- logger.error(`Failed to load or parse configuration from ${configFile} for getValuesFromFile: ${error.message}`);
125
+ // SECURITY FIX: Don't expose internal paths or detailed error information
126
+ logger.error('Failed to load or parse configuration file: ' + (error.message || 'Unknown error'));
34
127
  }
35
128
  }
36
129
  const config = clean({
package/dist/read.js.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"file":"read.js","sources":["../src/read.ts"],"sourcesContent":["import * as yaml from 'js-yaml';\nimport path from 'path';\nimport { z, ZodObject } from 'zod';\nimport { Args, ConfigSchema, Options } from './types';\nimport * as Storage from './util/storage';\n\nfunction clean(obj: any) {\n return Object.fromEntries(\n Object.entries(obj).filter(([_, v]) => v !== undefined)\n );\n}\n\nexport const read = async <T extends z.ZodRawShape>(args: Args, options: Options<T>): Promise<z.infer<ZodObject<T & typeof ConfigSchema.shape>>> => {\n const logger = options.logger;\n const storage = Storage.create({ log: logger.debug });\n\n const resolvedConfigDir = args.configDirectory || options.defaults?.configDirectory;\n logger.debug(`Resolved config directory: ${resolvedConfigDir}`);\n\n const configFile = path.join(resolvedConfigDir, options.defaults.configFile);\n logger.debug(`Attempting to load config file for cardigantime: ${configFile}`);\n\n let rawFileConfig: object = {};\n\n try {\n const yamlContent = await storage.readFile(configFile, options.defaults.encoding);\n const parsedYaml = yaml.load(yamlContent);\n if (parsedYaml !== null && typeof parsedYaml === 'object') {\n rawFileConfig = parsedYaml;\n logger.debug('Loaded Raw File Config for getValuesFromFile: \\n\\n%s\\n\\n', JSON.stringify(rawFileConfig, null, 2));\n } else if (parsedYaml !== null) {\n logger.warn(`Ignoring invalid configuration format in ${configFile} for cardigantime. Expected an object, got ${typeof parsedYaml}.`);\n }\n } catch (error: any) {\n if (error.code === 'ENOENT' || /not found|no such file/i.test(error.message)) {\n logger.debug(`Configuration file not found at ${configFile} for cardigantime. Returning empty object.`);\n } else {\n // Log error but don't throw, just return empty object as per the goal of just *getting* values\n logger.error(`Failed to load or parse configuration from ${configFile} for getValuesFromFile: ${error.message}`);\n }\n }\n\n const config: z.infer<ZodObject<T & typeof ConfigSchema.shape>> = clean({\n ...rawFileConfig,\n ...{\n configDirectory: resolvedConfigDir,\n }\n }) as z.infer<ZodObject<T & typeof ConfigSchema.shape>>;\n\n return config;\n}"],"names":["clean","obj","Object","fromEntries","entries","filter","_","v","undefined","read","args","options","logger","storage","Storage","log","debug","resolvedConfigDir","configDirectory","defaults","configFile","path","join","rawFileConfig","yamlContent","readFile","encoding","parsedYaml","yaml","load","JSON","stringify","warn","error","code","test","message","config"],"mappings":";;;;AAMA,SAASA,MAAMC,GAAQ,EAAA;AACnB,IAAA,OAAOC,MAAOC,CAAAA,WAAW,CACrBD,MAAAA,CAAOE,OAAO,CAACH,GAAAA,CAAAA,CAAKI,MAAM,CAAC,CAAC,CAACC,CAAGC,EAAAA,CAAAA,CAAE,GAAKA,CAAMC,KAAAA,SAAAA,CAAAA,CAAAA;AAErD;AAEO,MAAMC,IAAO,GAAA,OAAgCC,IAAYC,EAAAA,OAAAA,GAAAA;AAIVA,IAAAA,IAAAA,iBAAAA;IAHlD,MAAMC,MAAAA,GAASD,QAAQC,MAAM;IAC7B,MAAMC,OAAAA,GAAUC,MAAc,CAAC;AAAEC,QAAAA,GAAAA,EAAKH,OAAOI;AAAM,KAAA,CAAA;IAEnD,MAAMC,iBAAAA,GAAoBP,IAAKQ,CAAAA,eAAe,KAAIP,CAAAA,iBAAAA,GAAAA,QAAQQ,QAAQ,MAAA,IAAA,IAAhBR,iBAAAA,KAAAA,MAAAA,GAAAA,MAAAA,GAAAA,iBAAAA,CAAkBO,eAAe,CAAA;AACnFN,IAAAA,MAAAA,CAAOI,KAAK,CAAC,CAAC,2BAA2B,EAAEC,iBAAmB,CAAA,CAAA,CAAA;IAE9D,MAAMG,UAAAA,GAAaC,KAAKC,IAAI,CAACL,mBAAmBN,OAAQQ,CAAAA,QAAQ,CAACC,UAAU,CAAA;AAC3ER,IAAAA,MAAAA,CAAOI,KAAK,CAAC,CAAC,iDAAiD,EAAEI,UAAY,CAAA,CAAA,CAAA;AAE7E,IAAA,IAAIG,gBAAwB,EAAC;IAE7B,IAAI;QACA,MAAMC,WAAAA,GAAc,MAAMX,OAAQY,CAAAA,QAAQ,CAACL,UAAYT,EAAAA,OAAAA,CAAQQ,QAAQ,CAACO,QAAQ,CAAA;QAChF,MAAMC,UAAAA,GAAaC,IAAKC,CAAAA,IAAI,CAACL,WAAAA,CAAAA;AAC7B,QAAA,IAAIG,UAAe,KAAA,IAAA,IAAQ,OAAOA,UAAAA,KAAe,QAAU,EAAA;YACvDJ,aAAgBI,GAAAA,UAAAA;AAChBf,YAAAA,MAAAA,CAAOI,KAAK,CAAC,0DAAA,EAA4Dc,KAAKC,SAAS,CAACR,eAAe,IAAM,EAAA,CAAA,CAAA,CAAA;SAC1G,MAAA,IAAII,eAAe,IAAM,EAAA;YAC5Bf,MAAOoB,CAAAA,IAAI,CAAC,CAAC,yCAAyC,EAAEZ,UAAW,CAAA,2CAA2C,EAAE,OAAOO,UAAW,CAAA,CAAC,CAAC,CAAA;AACxI;AACJ,KAAA,CAAE,OAAOM,KAAY,EAAA;QACjB,IAAIA,KAAAA,CAAMC,IAAI,KAAK,QAAA,IAAY,0BAA0BC,IAAI,CAACF,KAAMG,CAAAA,OAAO,CAAG,EAAA;AAC1ExB,YAAAA,MAAAA,CAAOI,KAAK,CAAC,CAAC,gCAAgC,EAAEI,UAAAA,CAAW,0CAA0C,CAAC,CAAA;SACnG,MAAA;;YAEHR,MAAOqB,CAAAA,KAAK,CAAC,CAAC,2CAA2C,EAAEb,WAAW,wBAAwB,EAAEa,KAAMG,CAAAA,OAAO,CAAE,CAAA,CAAA;AACnH;AACJ;AAEA,IAAA,MAAMC,SAA4DrC,KAAM,CAAA;AACpE,QAAA,GAAGuB,aAAa;QAChB,GAAG;YACCL,eAAiBD,EAAAA;;AAEzB,KAAA,CAAA;IAEA,OAAOoB,MAAAA;AACX;;;;"}
1
+ {"version":3,"file":"read.js","sources":["../src/read.ts"],"sourcesContent":["import * as yaml from 'js-yaml';\nimport * as path from 'path';\nimport { z, ZodObject } from 'zod';\nimport { Args, ConfigSchema, Options } from './types';\nimport * as Storage from './util/storage';\n\n/**\n * Removes undefined values from an object to create a clean configuration.\n * This is used to merge configuration sources while avoiding undefined pollution.\n * \n * @param obj - The object to clean\n * @returns A new object with undefined values filtered out\n */\nfunction clean(obj: any) {\n return Object.fromEntries(\n Object.entries(obj).filter(([_, v]) => v !== undefined)\n );\n}\n\n/**\n * Validates and secures a user-provided path to prevent path traversal attacks.\n * \n * Security checks include:\n * - Path traversal prevention (blocks '..')\n * - Absolute path detection\n * - Path separator validation\n * \n * @param userPath - The user-provided path component\n * @param basePath - The base directory to join the path with\n * @returns The safely joined and normalized path\n * @throws {Error} When path traversal or absolute paths are detected\n */\nfunction validatePath(userPath: string, basePath: string): string {\n if (!userPath || !basePath) {\n throw new Error('Invalid path parameters');\n }\n\n const normalized = path.normalize(userPath);\n\n // Prevent path traversal attacks\n if (normalized.includes('..') || path.isAbsolute(normalized)) {\n throw new Error('Invalid path: path traversal detected');\n }\n\n // Ensure the path doesn't start with a path separator\n if (normalized.startsWith('/') || normalized.startsWith('\\\\')) {\n throw new Error('Invalid path: absolute path detected');\n }\n\n return path.join(basePath, normalized);\n}\n\n/**\n * Validates a configuration directory path for security and basic formatting.\n * \n * Performs validation to prevent:\n * - Null byte injection attacks\n * - Extremely long paths that could cause DoS\n * - Empty or invalid directory specifications\n * \n * @param configDir - The configuration directory path to validate\n * @returns The normalized configuration directory path\n * @throws {Error} When the directory path is invalid or potentially dangerous\n */\nfunction validateConfigDirectory(configDir: string): string {\n if (!configDir) {\n throw new Error('Configuration directory is required');\n }\n\n // Check for null bytes which could be used for path injection\n if (configDir.includes('\\0')) {\n throw new Error('Invalid path: null byte detected');\n }\n\n const normalized = path.normalize(configDir);\n\n // Basic validation - could be expanded based on requirements\n if (normalized.length > 1000) {\n throw new Error('Configuration directory path too long');\n }\n\n return normalized;\n}\n\n/**\n * Reads configuration from files and merges it with CLI arguments.\n * \n * This function implements the core configuration loading logic:\n * 1. Validates and resolves the configuration directory path\n * 2. Attempts to read the YAML configuration file\n * 3. Safely parses the YAML content with security protections\n * 4. Merges file configuration with runtime arguments\n * 5. Returns a typed configuration object\n * \n * The function handles missing files gracefully and provides detailed\n * logging for troubleshooting configuration issues.\n * \n * @template T - The Zod schema shape type for configuration validation\n * @param args - Parsed command-line arguments containing potential config overrides\n * @param options - Cardigantime options with defaults, schema, and logger\n * @returns Promise resolving to the merged and typed configuration object\n * @throws {Error} When configuration directory is invalid or required files cannot be read\n * \n * @example\n * ```typescript\n * const config = await read(cliArgs, {\n * defaults: { configDirectory: './config', configFile: 'app.yaml' },\n * configShape: MySchema.shape,\n * logger: console,\n * features: ['config']\n * });\n * // config is fully typed based on your schema\n * ```\n */\nexport const read = async <T extends z.ZodRawShape>(args: Args, options: Options<T>): Promise<z.infer<ZodObject<T & typeof ConfigSchema.shape>>> => {\n const logger = options.logger;\n const storage = Storage.create({ log: logger.debug });\n\n const rawConfigDir = args.configDirectory || options.defaults?.configDirectory;\n if (!rawConfigDir) {\n throw new Error('Configuration directory must be specified');\n }\n\n const resolvedConfigDir = validateConfigDirectory(rawConfigDir);\n logger.debug('Resolved config directory');\n\n const configFile = validatePath(options.defaults.configFile, resolvedConfigDir);\n logger.debug('Attempting to load config file for cardigantime');\n\n let rawFileConfig: object = {};\n\n try {\n const yamlContent = await storage.readFile(configFile, options.defaults.encoding);\n\n // SECURITY FIX: Use safer parsing options to prevent code execution vulnerabilities\n const parsedYaml = yaml.load(yamlContent);\n\n if (parsedYaml !== null && typeof parsedYaml === 'object') {\n rawFileConfig = parsedYaml;\n logger.debug('Loaded configuration file successfully');\n } else if (parsedYaml !== null) {\n logger.warn('Ignoring invalid configuration format. Expected an object, got ' + typeof parsedYaml);\n }\n } catch (error: any) {\n if (error.code === 'ENOENT' || /not found|no such file/i.test(error.message)) {\n logger.debug('Configuration file not found. Using empty configuration.');\n } else {\n // SECURITY FIX: Don't expose internal paths or detailed error information\n logger.error('Failed to load or parse configuration file: ' + (error.message || 'Unknown error'));\n }\n }\n\n const config: z.infer<ZodObject<T & typeof ConfigSchema.shape>> = clean({\n ...rawFileConfig,\n ...{\n configDirectory: resolvedConfigDir,\n }\n }) as z.infer<ZodObject<T & typeof ConfigSchema.shape>>;\n\n return config;\n}"],"names":["clean","obj","Object","fromEntries","entries","filter","_","v","undefined","validatePath","userPath","basePath","Error","normalized","path","normalize","includes","isAbsolute","startsWith","join","validateConfigDirectory","configDir","length","read","args","options","logger","storage","Storage","log","debug","rawConfigDir","configDirectory","defaults","resolvedConfigDir","configFile","rawFileConfig","yamlContent","readFile","encoding","parsedYaml","yaml","load","warn","error","code","test","message","config"],"mappings":"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"}
package/dist/types.d.ts CHANGED
@@ -1,40 +1,103 @@
1
1
  import { Command } from 'commander';
2
2
  import { ZodObject, z } from 'zod';
3
+ /**
4
+ * Available features that can be enabled in Cardigantime.
5
+ * Currently supports:
6
+ * - 'config': Configuration file reading and validation
7
+ */
3
8
  export type Feature = 'config';
9
+ /**
10
+ * Default configuration options for Cardigantime.
11
+ * These define the basic behavior of configuration loading.
12
+ */
4
13
  export interface DefaultOptions {
14
+ /** Directory path where configuration files are located */
5
15
  configDirectory: string;
16
+ /** Name of the configuration file (e.g., 'config.yaml', 'app.yml') */
6
17
  configFile: string;
18
+ /** Whether the configuration directory must exist. If true, throws error if directory doesn't exist */
7
19
  isRequired: boolean;
20
+ /** File encoding for reading configuration files (e.g., 'utf8', 'ascii') */
8
21
  encoding: string;
9
22
  }
23
+ /**
24
+ * Complete options object passed to Cardigantime functions.
25
+ * Combines defaults, features, schema shape, and logger.
26
+ *
27
+ * @template T - The Zod schema shape type for configuration validation
28
+ */
10
29
  export interface Options<T extends z.ZodRawShape> {
30
+ /** Default configuration options */
11
31
  defaults: DefaultOptions;
32
+ /** Array of enabled features */
12
33
  features: Feature[];
34
+ /** Zod schema shape for validating user configuration */
13
35
  configShape: T;
36
+ /** Logger instance for debugging and error reporting */
14
37
  logger: Logger;
15
38
  }
39
+ /**
40
+ * Logger interface for Cardigantime's internal logging.
41
+ * Compatible with popular logging libraries like Winston, Bunyan, etc.
42
+ */
16
43
  export interface Logger {
44
+ /** Debug-level logging for detailed troubleshooting information */
17
45
  debug: (message: string, ...args: any[]) => void;
46
+ /** Info-level logging for general information */
18
47
  info: (message: string, ...args: any[]) => void;
48
+ /** Warning-level logging for non-critical issues */
19
49
  warn: (message: string, ...args: any[]) => void;
50
+ /** Error-level logging for critical problems */
20
51
  error: (message: string, ...args: any[]) => void;
52
+ /** Verbose-level logging for extensive detail */
21
53
  verbose: (message: string, ...args: any[]) => void;
54
+ /** Silly-level logging for maximum detail */
22
55
  silly: (message: string, ...args: any[]) => void;
23
56
  }
57
+ /**
58
+ * Main Cardigantime interface providing configuration management functionality.
59
+ *
60
+ * @template T - The Zod schema shape type for configuration validation
61
+ */
24
62
  export interface Cardigantime<T extends z.ZodRawShape> {
63
+ /**
64
+ * Adds Cardigantime's CLI options to a Commander.js command.
65
+ * This includes options like --config-directory for runtime config path overrides.
66
+ */
25
67
  configure: (command: Command) => Promise<Command>;
68
+ /** Sets a custom logger for debugging and error reporting */
26
69
  setLogger: (logger: Logger) => void;
70
+ /**
71
+ * Reads configuration from files and merges with CLI arguments.
72
+ * Returns a fully typed configuration object.
73
+ */
27
74
  read: (args: Args) => Promise<z.infer<ZodObject<T & typeof ConfigSchema.shape>>>;
75
+ /**
76
+ * Validates the merged configuration against the Zod schema.
77
+ * Throws ConfigurationError if validation fails.
78
+ */
28
79
  validate: (config: z.infer<ZodObject<T & typeof ConfigSchema.shape>>) => Promise<void>;
29
80
  }
81
+ /**
82
+ * Parsed command-line arguments object, typically from Commander.js opts().
83
+ * Keys correspond to CLI option names with values from user input.
84
+ */
30
85
  export interface Args {
31
86
  [key: string]: any;
32
87
  }
88
+ /**
89
+ * Base Zod schema for core Cardigantime configuration.
90
+ * Contains the minimum required configuration fields.
91
+ */
33
92
  export declare const ConfigSchema: ZodObject<{
93
+ /** The resolved configuration directory path */
34
94
  configDirectory: z.ZodString;
35
95
  }, "strip", z.ZodTypeAny, {
36
96
  configDirectory: string;
37
97
  }, {
38
98
  configDirectory: string;
39
99
  }>;
100
+ /**
101
+ * Base configuration type derived from the core schema.
102
+ */
40
103
  export type Config = z.infer<typeof ConfigSchema>;
package/dist/types.js CHANGED
@@ -1,8 +1,10 @@
1
1
  import { z } from 'zod';
2
2
 
3
- // Base schema for core options
4
- const ConfigSchema = z.object({
5
- configDirectory: z.string()
3
+ /**
4
+ * Base Zod schema for core Cardigantime configuration.
5
+ * Contains the minimum required configuration fields.
6
+ */ const ConfigSchema = z.object({
7
+ /** The resolved configuration directory path */ configDirectory: z.string()
6
8
  });
7
9
 
8
10
  export { ConfigSchema };
package/dist/types.js.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"file":"types.js","sources":["../src/types.ts"],"sourcesContent":["import { Command } from \"commander\";\nimport { ZodObject } from \"zod\";\n\nimport { z } from \"zod\";\n\nexport type Feature = 'config';\n\nexport interface DefaultOptions {\n configDirectory: string;\n configFile: string;\n isRequired: boolean;\n encoding: string;\n}\n\n// Use ZodRawShape for easier merging later\nexport interface Options<T extends z.ZodRawShape> {\n defaults: DefaultOptions,\n features: Feature[],\n configShape: T; // User-defined configuration shape\n logger: Logger;\n}\n\nexport interface Logger {\n debug: (message: string, ...args: any[]) => void;\n info: (message: string, ...args: any[]) => void;\n warn: (message: string, ...args: any[]) => void;\n error: (message: string, ...args: any[]) => void;\n verbose: (message: string, ...args: any[]) => void;\n silly: (message: string, ...args: any[]) => void;\n}\n\n// Make interface generic\nexport interface Cardigantime<T extends z.ZodRawShape> {\n configure: (command: Command) => Promise<Command>;\n setLogger: (logger: Logger) => void;\n read: (args: Args) => Promise<z.infer<ZodObject<T & typeof ConfigSchema.shape>>>;\n validate: (config: z.infer<ZodObject<T & typeof ConfigSchema.shape>>) => Promise<void>;\n}\n\nexport interface Args {\n [key: string]: any;\n}\n\n// Base schema for core options\nexport const ConfigSchema = z.object({\n configDirectory: z.string(),\n});\n\nexport type Config = z.infer<typeof ConfigSchema>;\n"],"names":["ConfigSchema","z","object","configDirectory","string"],"mappings":";;AA2CA;AACaA,MAAAA,YAAAA,GAAeC,CAAEC,CAAAA,MAAM,CAAC;AACjCC,IAAAA,eAAAA,EAAiBF,EAAEG,MAAM;AAC7B,CAAG;;;;"}
1
+ {"version":3,"file":"types.js","sources":["../src/types.ts"],"sourcesContent":["import { Command } from \"commander\";\nimport { ZodObject } from \"zod\";\n\nimport { z } from \"zod\";\n\n/**\n * Available features that can be enabled in Cardigantime.\n * Currently supports:\n * - 'config': Configuration file reading and validation\n */\nexport type Feature = 'config';\n\n/**\n * Default configuration options for Cardigantime.\n * These define the basic behavior of configuration loading.\n */\nexport interface DefaultOptions {\n /** Directory path where configuration files are located */\n configDirectory: string;\n /** Name of the configuration file (e.g., 'config.yaml', 'app.yml') */\n configFile: string;\n /** Whether the configuration directory must exist. If true, throws error if directory doesn't exist */\n isRequired: boolean;\n /** File encoding for reading configuration files (e.g., 'utf8', 'ascii') */\n encoding: string;\n}\n\n/**\n * Complete options object passed to Cardigantime functions.\n * Combines defaults, features, schema shape, and logger.\n * \n * @template T - The Zod schema shape type for configuration validation\n */\nexport interface Options<T extends z.ZodRawShape> {\n /** Default configuration options */\n defaults: DefaultOptions,\n /** Array of enabled features */\n features: Feature[],\n /** Zod schema shape for validating user configuration */\n configShape: T;\n /** Logger instance for debugging and error reporting */\n logger: Logger;\n}\n\n/**\n * Logger interface for Cardigantime's internal logging.\n * Compatible with popular logging libraries like Winston, Bunyan, etc.\n */\nexport interface Logger {\n /** Debug-level logging for detailed troubleshooting information */\n debug: (message: string, ...args: any[]) => void;\n /** Info-level logging for general information */\n info: (message: string, ...args: any[]) => void;\n /** Warning-level logging for non-critical issues */\n warn: (message: string, ...args: any[]) => void;\n /** Error-level logging for critical problems */\n error: (message: string, ...args: any[]) => void;\n /** Verbose-level logging for extensive detail */\n verbose: (message: string, ...args: any[]) => void;\n /** Silly-level logging for maximum detail */\n silly: (message: string, ...args: any[]) => void;\n}\n\n/**\n * Main Cardigantime interface providing configuration management functionality.\n * \n * @template T - The Zod schema shape type for configuration validation\n */\nexport interface Cardigantime<T extends z.ZodRawShape> {\n /** \n * Adds Cardigantime's CLI options to a Commander.js command.\n * This includes options like --config-directory for runtime config path overrides.\n */\n configure: (command: Command) => Promise<Command>;\n /** Sets a custom logger for debugging and error reporting */\n setLogger: (logger: Logger) => void;\n /** \n * Reads configuration from files and merges with CLI arguments.\n * Returns a fully typed configuration object.\n */\n read: (args: Args) => Promise<z.infer<ZodObject<T & typeof ConfigSchema.shape>>>;\n /** \n * Validates the merged configuration against the Zod schema.\n * Throws ConfigurationError if validation fails.\n */\n validate: (config: z.infer<ZodObject<T & typeof ConfigSchema.shape>>) => Promise<void>;\n}\n\n/**\n * Parsed command-line arguments object, typically from Commander.js opts().\n * Keys correspond to CLI option names with values from user input.\n */\nexport interface Args {\n [key: string]: any;\n}\n\n/**\n * Base Zod schema for core Cardigantime configuration.\n * Contains the minimum required configuration fields.\n */\nexport const ConfigSchema = z.object({\n /** The resolved configuration directory path */\n configDirectory: z.string(),\n});\n\n/**\n * Base configuration type derived from the core schema.\n */\nexport type Config = z.infer<typeof ConfigSchema>;\n"],"names":["ConfigSchema","z","object","configDirectory","string"],"mappings":";;AAgGA;;;AAGC,IACM,MAAMA,YAAAA,GAAeC,CAAAA,CAAEC,MAAM,CAAC;qDAEjCC,eAAAA,EAAiBF,CAAAA,CAAEG,MAAM;AAC7B,CAAA;;;;"}
@@ -1,7 +1,8 @@
1
1
  import * as fs from 'fs';
2
2
  import { glob } from 'glob';
3
- import path from 'path';
3
+ import path__default from 'path';
4
4
  import crypto from 'crypto';
5
+ import { FileSystemError } from '../error/FileSystemError.js';
5
6
 
6
7
  // eslint-disable-next-line no-restricted-imports
7
8
  const create = (params)=>{
@@ -65,10 +66,38 @@ const create = (params)=>{
65
66
  recursive: true
66
67
  });
67
68
  } catch (mkdirError) {
68
- throw new Error(`Failed to create output directory ${path}: ${mkdirError.message} ${mkdirError.stack}`);
69
+ throw FileSystemError.directoryCreationFailed(path, mkdirError);
69
70
  }
70
71
  };
71
72
  const readFile = async (path, encoding)=>{
73
+ // Validate encoding parameter
74
+ const validEncodings = [
75
+ 'utf8',
76
+ 'utf-8',
77
+ 'ascii',
78
+ 'latin1',
79
+ 'base64',
80
+ 'hex',
81
+ 'utf16le',
82
+ 'ucs2',
83
+ 'ucs-2'
84
+ ];
85
+ if (!validEncodings.includes(encoding.toLowerCase())) {
86
+ throw new Error('Invalid encoding specified');
87
+ }
88
+ // Check file size before reading to prevent DoS
89
+ try {
90
+ const stats = await fs.promises.stat(path);
91
+ const maxFileSize = 10 * 1024 * 1024; // 10MB limit
92
+ if (stats.size > maxFileSize) {
93
+ throw new Error('File too large to process');
94
+ }
95
+ } catch (error) {
96
+ if (error.code === 'ENOENT') {
97
+ throw FileSystemError.fileNotFound(path);
98
+ }
99
+ throw error;
100
+ }
72
101
  return await fs.promises.readFile(path, {
73
102
  encoding: encoding
74
103
  });
@@ -87,10 +116,10 @@ const create = (params)=>{
87
116
  nodir: true
88
117
  });
89
118
  for (const file of files){
90
- await callback(path.join(directory, file));
119
+ await callback(path__default.join(directory, file));
91
120
  }
92
121
  } catch (err) {
93
- throw new Error(`Failed to glob pattern ${options.pattern} in ${directory}: ${err.message}`);
122
+ throw FileSystemError.operationFailed(`glob pattern ${options.pattern}`, directory, err);
94
123
  }
95
124
  };
96
125
  const readStream = async (path)=>{
@@ -1 +1 @@
1
- {"version":3,"file":"storage.js","sources":["../../src/util/storage.ts"],"sourcesContent":["// eslint-disable-next-line no-restricted-imports\nimport * as fs from 'fs';\nimport { glob } from 'glob';\nimport path from 'path';\nimport crypto from 'crypto';\n/**\n * This module exists to isolate filesystem operations from the rest of the codebase.\n * This makes testing easier by avoiding direct fs mocking in jest configuration.\n * \n * Additionally, abstracting storage operations allows for future flexibility - \n * this export utility may need to work with storage systems other than the local filesystem\n * (e.g. S3, Google Cloud Storage, etc).\n */\n\nexport interface Utility {\n exists: (path: string) => Promise<boolean>;\n isDirectory: (path: string) => Promise<boolean>;\n isFile: (path: string) => Promise<boolean>;\n isReadable: (path: string) => Promise<boolean>;\n isWritable: (path: string) => Promise<boolean>;\n isFileReadable: (path: string) => Promise<boolean>;\n isDirectoryWritable: (path: string) => Promise<boolean>;\n isDirectoryReadable: (path: string) => Promise<boolean>;\n createDirectory: (path: string) => Promise<void>;\n readFile: (path: string, encoding: string) => Promise<string>;\n readStream: (path: string) => Promise<fs.ReadStream>;\n writeFile: (path: string, data: string | Buffer, encoding: string) => Promise<void>;\n forEachFileIn: (directory: string, callback: (path: string) => Promise<void>, options?: { pattern: string }) => Promise<void>;\n hashFile: (path: string, length: number) => Promise<string>;\n listFiles: (directory: string) => Promise<string[]>;\n}\n\nexport const create = (params: { log?: (message: string, ...args: any[]) => void }): Utility => {\n\n // eslint-disable-next-line no-console\n const log = params.log || console.log;\n\n const exists = async (path: string): Promise<boolean> => {\n try {\n await fs.promises.stat(path);\n return true;\n // eslint-disable-next-line @typescript-eslint/no-unused-vars\n } catch (error: any) {\n return false;\n }\n }\n\n const isDirectory = async (path: string): Promise<boolean> => {\n const stats = await fs.promises.stat(path);\n if (!stats.isDirectory()) {\n log(`${path} is not a directory`);\n return false;\n }\n return true;\n }\n\n const isFile = async (path: string): Promise<boolean> => {\n const stats = await fs.promises.stat(path);\n if (!stats.isFile()) {\n log(`${path} is not a file`);\n return false;\n }\n return true;\n }\n\n const isReadable = async (path: string): Promise<boolean> => {\n try {\n await fs.promises.access(path, fs.constants.R_OK);\n } catch (error: any) {\n log(`${path} is not readable: %s %s`, error.message, error.stack);\n return false;\n }\n return true;\n }\n\n const isWritable = async (path: string): Promise<boolean> => {\n try {\n await fs.promises.access(path, fs.constants.W_OK);\n } catch (error: any) {\n log(`${path} is not writable: %s %s`, error.message, error.stack);\n return false;\n }\n return true;\n }\n\n const isFileReadable = async (path: string): Promise<boolean> => {\n return await exists(path) && await isFile(path) && await isReadable(path);\n }\n\n const isDirectoryWritable = async (path: string): Promise<boolean> => {\n return await exists(path) && await isDirectory(path) && await isWritable(path);\n }\n\n const isDirectoryReadable = async (path: string): Promise<boolean> => {\n return await exists(path) && await isDirectory(path) && await isReadable(path);\n }\n\n const createDirectory = async (path: string): Promise<void> => {\n try {\n await fs.promises.mkdir(path, { recursive: true });\n } catch (mkdirError: any) {\n throw new Error(`Failed to create output directory ${path}: ${mkdirError.message} ${mkdirError.stack}`);\n }\n }\n\n const readFile = async (path: string, encoding: string): Promise<string> => {\n return await fs.promises.readFile(path, { encoding: encoding as BufferEncoding });\n }\n\n const writeFile = async (path: string, data: string | Buffer, encoding: string): Promise<void> => {\n await fs.promises.writeFile(path, data, { encoding: encoding as BufferEncoding });\n }\n\n const forEachFileIn = async (directory: string, callback: (file: string) => Promise<void>, options: { pattern: string | string[] } = { pattern: '*.*' }): Promise<void> => {\n try {\n const files = await glob(options.pattern, { cwd: directory, nodir: true });\n for (const file of files) {\n await callback(path.join(directory, file));\n }\n } catch (err: any) {\n throw new Error(`Failed to glob pattern ${options.pattern} in ${directory}: ${err.message}`);\n }\n }\n\n const readStream = async (path: string): Promise<fs.ReadStream> => {\n return fs.createReadStream(path);\n }\n\n const hashFile = async (path: string, length: number): Promise<string> => {\n const file = await readFile(path, 'utf8');\n return crypto.createHash('sha256').update(file).digest('hex').slice(0, length);\n }\n\n const listFiles = async (directory: string): Promise<string[]> => {\n return await fs.promises.readdir(directory);\n }\n\n return {\n exists,\n isDirectory,\n isFile,\n isReadable,\n isWritable,\n isFileReadable,\n isDirectoryWritable,\n isDirectoryReadable,\n createDirectory,\n readFile,\n readStream,\n writeFile,\n forEachFileIn,\n hashFile,\n listFiles,\n };\n}"],"names":["create","params","log","console","exists","path","fs","promises","stat","error","isDirectory","stats","isFile","isReadable","access","constants","R_OK","message","stack","isWritable","W_OK","isFileReadable","isDirectoryWritable","isDirectoryReadable","createDirectory","mkdir","recursive","mkdirError","Error","readFile","encoding","writeFile","data","forEachFileIn","directory","callback","options","pattern","files","glob","cwd","nodir","file","join","err","readStream","createReadStream","hashFile","length","crypto","createHash","update","digest","slice","listFiles","readdir"],"mappings":"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"}
1
+ {"version":3,"file":"storage.js","sources":["../../src/util/storage.ts"],"sourcesContent":["// eslint-disable-next-line no-restricted-imports\nimport * as fs from 'fs';\nimport { glob } from 'glob';\nimport path from 'path';\nimport crypto from 'crypto';\nimport { FileSystemError } from '../error/FileSystemError';\n/**\n * This module exists to isolate filesystem operations from the rest of the codebase.\n * This makes testing easier by avoiding direct fs mocking in jest configuration.\n * \n * Additionally, abstracting storage operations allows for future flexibility - \n * this export utility may need to work with storage systems other than the local filesystem\n * (e.g. S3, Google Cloud Storage, etc).\n */\n\nexport interface Utility {\n exists: (path: string) => Promise<boolean>;\n isDirectory: (path: string) => Promise<boolean>;\n isFile: (path: string) => Promise<boolean>;\n isReadable: (path: string) => Promise<boolean>;\n isWritable: (path: string) => Promise<boolean>;\n isFileReadable: (path: string) => Promise<boolean>;\n isDirectoryWritable: (path: string) => Promise<boolean>;\n isDirectoryReadable: (path: string) => Promise<boolean>;\n createDirectory: (path: string) => Promise<void>;\n readFile: (path: string, encoding: string) => Promise<string>;\n readStream: (path: string) => Promise<fs.ReadStream>;\n writeFile: (path: string, data: string | Buffer, encoding: string) => Promise<void>;\n forEachFileIn: (directory: string, callback: (path: string) => Promise<void>, options?: { pattern: string }) => Promise<void>;\n hashFile: (path: string, length: number) => Promise<string>;\n listFiles: (directory: string) => Promise<string[]>;\n}\n\nexport const create = (params: { log?: (message: string, ...args: any[]) => void }): Utility => {\n\n // eslint-disable-next-line no-console\n const log = params.log || console.log;\n\n const exists = async (path: string): Promise<boolean> => {\n try {\n await fs.promises.stat(path);\n return true;\n // eslint-disable-next-line @typescript-eslint/no-unused-vars\n } catch (error: any) {\n return false;\n }\n }\n\n const isDirectory = async (path: string): Promise<boolean> => {\n const stats = await fs.promises.stat(path);\n if (!stats.isDirectory()) {\n log(`${path} is not a directory`);\n return false;\n }\n return true;\n }\n\n const isFile = async (path: string): Promise<boolean> => {\n const stats = await fs.promises.stat(path);\n if (!stats.isFile()) {\n log(`${path} is not a file`);\n return false;\n }\n return true;\n }\n\n const isReadable = async (path: string): Promise<boolean> => {\n try {\n await fs.promises.access(path, fs.constants.R_OK);\n } catch (error: any) {\n log(`${path} is not readable: %s %s`, error.message, error.stack);\n return false;\n }\n return true;\n }\n\n const isWritable = async (path: string): Promise<boolean> => {\n try {\n await fs.promises.access(path, fs.constants.W_OK);\n } catch (error: any) {\n log(`${path} is not writable: %s %s`, error.message, error.stack);\n return false;\n }\n return true;\n }\n\n const isFileReadable = async (path: string): Promise<boolean> => {\n return await exists(path) && await isFile(path) && await isReadable(path);\n }\n\n const isDirectoryWritable = async (path: string): Promise<boolean> => {\n return await exists(path) && await isDirectory(path) && await isWritable(path);\n }\n\n const isDirectoryReadable = async (path: string): Promise<boolean> => {\n return await exists(path) && await isDirectory(path) && await isReadable(path);\n }\n\n const createDirectory = async (path: string): Promise<void> => {\n try {\n await fs.promises.mkdir(path, { recursive: true });\n } catch (mkdirError: any) {\n throw FileSystemError.directoryCreationFailed(path, mkdirError);\n }\n }\n\n const readFile = async (path: string, encoding: string): Promise<string> => {\n // Validate encoding parameter\n const validEncodings = ['utf8', 'utf-8', 'ascii', 'latin1', 'base64', 'hex', 'utf16le', 'ucs2', 'ucs-2'];\n if (!validEncodings.includes(encoding.toLowerCase())) {\n throw new Error('Invalid encoding specified');\n }\n\n // Check file size before reading to prevent DoS\n try {\n const stats = await fs.promises.stat(path);\n const maxFileSize = 10 * 1024 * 1024; // 10MB limit\n if (stats.size > maxFileSize) {\n throw new Error('File too large to process');\n }\n } catch (error: any) {\n if (error.code === 'ENOENT') {\n throw FileSystemError.fileNotFound(path);\n }\n throw error;\n }\n\n return await fs.promises.readFile(path, { encoding: encoding as BufferEncoding });\n }\n\n const writeFile = async (path: string, data: string | Buffer, encoding: string): Promise<void> => {\n await fs.promises.writeFile(path, data, { encoding: encoding as BufferEncoding });\n }\n\n const forEachFileIn = async (directory: string, callback: (file: string) => Promise<void>, options: { pattern: string | string[] } = { pattern: '*.*' }): Promise<void> => {\n try {\n const files = await glob(options.pattern, { cwd: directory, nodir: true });\n for (const file of files) {\n await callback(path.join(directory, file));\n }\n } catch (err: any) {\n throw FileSystemError.operationFailed(`glob pattern ${options.pattern}`, directory, err);\n }\n }\n\n const readStream = async (path: string): Promise<fs.ReadStream> => {\n return fs.createReadStream(path);\n }\n\n const hashFile = async (path: string, length: number): Promise<string> => {\n const file = await readFile(path, 'utf8');\n return crypto.createHash('sha256').update(file).digest('hex').slice(0, length);\n }\n\n const listFiles = async (directory: string): Promise<string[]> => {\n return await fs.promises.readdir(directory);\n }\n\n return {\n exists,\n isDirectory,\n isFile,\n isReadable,\n isWritable,\n isFileReadable,\n isDirectoryWritable,\n isDirectoryReadable,\n createDirectory,\n readFile,\n readStream,\n writeFile,\n forEachFileIn,\n hashFile,\n listFiles,\n };\n}"],"names":["create","params","log","console","exists","path","fs","promises","stat","error","isDirectory","stats","isFile","isReadable","access","constants","R_OK","message","stack","isWritable","W_OK","isFileReadable","isDirectoryWritable","isDirectoryReadable","createDirectory","mkdir","recursive","mkdirError","FileSystemError","directoryCreationFailed","readFile","encoding","validEncodings","includes","toLowerCase","Error","maxFileSize","size","code","fileNotFound","writeFile","data","forEachFileIn","directory","callback","options","pattern","files","glob","cwd","nodir","file","join","err","operationFailed","readStream","createReadStream","hashFile","length","crypto","createHash","update","digest","slice","listFiles","readdir"],"mappings":"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"}
@@ -1,7 +1,41 @@
1
1
  import { z, ZodObject } from 'zod';
2
2
  import { ArgumentError } from './error/ArgumentError';
3
+ import { ConfigurationError } from './error/ConfigurationError';
4
+ import { FileSystemError } from './error/FileSystemError';
3
5
  import { ConfigSchema, Logger, Options } from './types';
4
- export { ArgumentError };
6
+ export { ArgumentError, ConfigurationError, FileSystemError };
7
+ /**
8
+ * Recursively extracts all keys from a Zod schema in dot notation.
9
+ *
10
+ * This function traverses a Zod schema structure and builds a flat list
11
+ * of all possible keys, using dot notation for nested objects. It handles
12
+ * optional/nullable types by unwrapping them and supports arrays by
13
+ * introspecting their element type.
14
+ *
15
+ * Special handling for:
16
+ * - ZodOptional/ZodNullable: Unwraps to get the underlying type
17
+ * - ZodAny/ZodRecord: Accepts any keys, returns the prefix or empty array
18
+ * - ZodArray: Introspects the element type
19
+ * - ZodObject: Recursively processes all shape properties
20
+ *
21
+ * @param schema - The Zod schema to introspect
22
+ * @param prefix - Internal parameter for building nested key paths
23
+ * @returns Array of strings representing all possible keys in dot notation
24
+ *
25
+ * @example
26
+ * ```typescript
27
+ * const schema = z.object({
28
+ * user: z.object({
29
+ * name: z.string(),
30
+ * settings: z.object({ theme: z.string() })
31
+ * }),
32
+ * debug: z.boolean()
33
+ * });
34
+ *
35
+ * const keys = listZodKeys(schema);
36
+ * // Returns: ['user.name', 'user.settings.theme', 'debug']
37
+ * ```
38
+ */
5
39
  export declare const listZodKeys: (schema: z.ZodTypeAny, prefix?: string) => string[];
6
40
  /**
7
41
  * Generates a list of all keys within a JavaScript object, using dot notation for nested keys.
@@ -14,5 +48,66 @@ export declare const listZodKeys: (schema: z.ZodTypeAny, prefix?: string) => str
14
48
  * @returns An array of strings representing all keys in dot notation.
15
49
  */
16
50
  export declare const listObjectKeys: (obj: Record<string, unknown>, prefix?: string) => string[];
51
+ /**
52
+ * Validates that the configuration object contains only keys allowed by the schema.
53
+ *
54
+ * This function prevents configuration errors by detecting typos or extra keys
55
+ * that aren't defined in the Zod schema. It intelligently handles:
56
+ * - ZodRecord types that accept arbitrary keys
57
+ * - Nested objects and their key structures
58
+ * - Arrays and their element key structures
59
+ *
60
+ * The function throws a ConfigurationError if extra keys are found, providing
61
+ * helpful information about what keys are allowed vs. what was found.
62
+ *
63
+ * @param mergedSources - The merged configuration object to validate
64
+ * @param fullSchema - The complete Zod schema including base and user schemas
65
+ * @param logger - Logger for error reporting
66
+ * @throws {ConfigurationError} When extra keys are found that aren't in the schema
67
+ *
68
+ * @example
69
+ * ```typescript
70
+ * const schema = z.object({ name: z.string(), age: z.number() });
71
+ * const config = { name: 'John', age: 30, typo: 'invalid' };
72
+ *
73
+ * checkForExtraKeys(config, schema, console);
74
+ * // Throws: ConfigurationError with details about 'typo' being an extra key
75
+ * ```
76
+ */
17
77
  export declare const checkForExtraKeys: (mergedSources: object, fullSchema: ZodObject<any>, logger: Logger | typeof console) => void;
78
+ /**
79
+ * Validates a configuration object against the combined Zod schema.
80
+ *
81
+ * This is the main validation function that:
82
+ * 1. Validates the configuration directory (if config feature enabled)
83
+ * 2. Combines the base ConfigSchema with user-provided schema shape
84
+ * 3. Checks for extra keys not defined in the schema
85
+ * 4. Validates all values against their schema definitions
86
+ * 5. Provides detailed error reporting for validation failures
87
+ *
88
+ * The validation is comprehensive and catches common configuration errors
89
+ * including typos, missing required fields, wrong types, and invalid values.
90
+ *
91
+ * @template T - The Zod schema shape type for configuration validation
92
+ * @param config - The merged configuration object to validate
93
+ * @param options - Cardigantime options containing schema, defaults, and logger
94
+ * @throws {ConfigurationError} When configuration validation fails
95
+ * @throws {FileSystemError} When configuration directory validation fails
96
+ *
97
+ * @example
98
+ * ```typescript
99
+ * const schema = z.object({
100
+ * apiKey: z.string().min(1),
101
+ * timeout: z.number().positive(),
102
+ * });
103
+ *
104
+ * await validate(config, {
105
+ * configShape: schema.shape,
106
+ * defaults: { configDirectory: './config', isRequired: true },
107
+ * logger: console,
108
+ * features: ['config']
109
+ * });
110
+ * // Throws detailed errors if validation fails
111
+ * ```
112
+ */
18
113
  export declare const validate: <T extends z.ZodRawShape>(config: z.infer<ZodObject<T & typeof ConfigSchema.shape>>, options: Options<T>) => Promise<void>;