@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
@@ -2,21 +2,910 @@
2
2
 
3
3
  Object.defineProperty(exports, Symbol.toStringTag, { value: 'Module' });
4
4
 
5
- const configure = require('./configure.cjs');
6
- const constants = require('./constants.cjs');
7
- const read = require('./read.cjs');
8
- const validate = require('./validate.cjs');
9
- const types = require('./types.cjs');
10
-
11
- // Make create function generic
12
- const create = (pOptions)=>{
5
+ const yaml = require('js-yaml');
6
+ const path = require('path');
7
+ const fs = require('fs');
8
+ const glob = require('glob');
9
+ const crypto = require('crypto');
10
+ const zod = require('zod');
11
+
12
+ function _interopNamespaceDefault(e) {
13
+ const n = Object.create(null, { [Symbol.toStringTag]: { value: 'Module' } });
14
+ if (e) {
15
+ for (const k in e) {
16
+ if (k !== 'default') {
17
+ const d = Object.getOwnPropertyDescriptor(e, k);
18
+ Object.defineProperty(n, k, d.get ? d : {
19
+ enumerable: true,
20
+ get: () => e[k]
21
+ });
22
+ }
23
+ }
24
+ }
25
+ n.default = e;
26
+ return Object.freeze(n);
27
+ }
28
+
29
+ const yaml__namespace = /*#__PURE__*/_interopNamespaceDefault(yaml);
30
+ const path__namespace = /*#__PURE__*/_interopNamespaceDefault(path);
31
+ const fs__namespace = /*#__PURE__*/_interopNamespaceDefault(fs);
32
+
33
+ /**
34
+ * Error thrown when CLI arguments or function parameters are invalid.
35
+ *
36
+ * This error provides specific context about which argument failed validation
37
+ * and why, making it easier for users to fix their command-line usage or
38
+ * for developers to debug parameter issues.
39
+ *
40
+ * @example
41
+ * ```typescript
42
+ * throw new ArgumentError('config-directory', 'Path cannot be empty');
43
+ * // Error message: "Path cannot be empty"
44
+ * // error.argument: "config-directory"
45
+ * ```
46
+ */ function _define_property$2(obj, key, value) {
47
+ if (key in obj) {
48
+ Object.defineProperty(obj, key, {
49
+ value: value,
50
+ enumerable: true,
51
+ configurable: true,
52
+ writable: true
53
+ });
54
+ } else {
55
+ obj[key] = value;
56
+ }
57
+ return obj;
58
+ }
59
+ class ArgumentError extends Error {
60
+ /**
61
+ * Gets the name of the argument that caused this error.
62
+ *
63
+ * @returns The argument name
64
+ */ get argument() {
65
+ return this.argumentName;
66
+ }
67
+ /**
68
+ * Creates a new ArgumentError instance.
69
+ *
70
+ * @param argumentName - The name of the invalid argument
71
+ * @param message - Description of why the argument is invalid
72
+ */ constructor(argumentName, message){
73
+ super(`${message}`), /** The name of the argument that caused the error */ _define_property$2(this, "argumentName", void 0);
74
+ this.name = 'ArgumentError';
75
+ this.argumentName = argumentName;
76
+ }
77
+ }
78
+
79
+ /**
80
+ * Validates a configuration directory path to ensure it's safe and valid.
81
+ *
82
+ * Performs security and safety checks including:
83
+ * - Non-empty string validation
84
+ * - Null byte injection prevention
85
+ * - Path length validation
86
+ * - Type checking
87
+ *
88
+ * @param configDirectory - The configuration directory path to validate
89
+ * @param _testThrowNonArgumentError - Internal testing parameter to simulate non-ArgumentError exceptions
90
+ * @returns The trimmed and validated configuration directory path
91
+ * @throws {ArgumentError} When the directory path is invalid
92
+ *
93
+ * @example
94
+ * ```typescript
95
+ * const validDir = validateConfigDirectory('./config'); // Returns './config'
96
+ * const invalidDir = validateConfigDirectory(''); // Throws ArgumentError
97
+ * ```
98
+ */ function validateConfigDirectory$2(configDirectory, _testThrowNonArgumentError) {
99
+ if (!configDirectory) {
100
+ throw new ArgumentError('configDirectory', 'Configuration directory cannot be empty');
101
+ }
102
+ if (typeof configDirectory !== 'string') {
103
+ throw new ArgumentError('configDirectory', 'Configuration directory must be a string');
104
+ }
105
+ const trimmed = configDirectory.trim();
106
+ if (trimmed.length === 0) {
107
+ throw new ArgumentError('configDirectory', 'Configuration directory cannot be empty or whitespace only');
108
+ }
109
+ // Check for obviously invalid paths
110
+ if (trimmed.includes('\0')) {
111
+ throw new ArgumentError('configDirectory', 'Configuration directory contains invalid null character');
112
+ }
113
+ // Validate path length (reasonable limit)
114
+ if (trimmed.length > 1000) {
115
+ throw new ArgumentError('configDirectory', 'Configuration directory path is too long (max 1000 characters)');
116
+ }
117
+ return trimmed;
118
+ }
119
+ /**
120
+ * Configures a Commander.js command with Cardigantime's CLI options.
121
+ *
122
+ * This function adds command-line options that allow users to override
123
+ * configuration settings at runtime, such as:
124
+ * - --config-directory: Override the default configuration directory
125
+ *
126
+ * The function validates both the command object and the options to ensure
127
+ * they meet the requirements for proper integration.
128
+ *
129
+ * @template T - The Zod schema shape type for configuration validation
130
+ * @param command - The Commander.js Command instance to configure
131
+ * @param options - Cardigantime options containing defaults and schema
132
+ * @param _testThrowNonArgumentError - Internal testing parameter
133
+ * @returns Promise resolving to the configured Command instance
134
+ * @throws {ArgumentError} When command or options are invalid
135
+ *
136
+ * @example
137
+ * ```typescript
138
+ * import { Command } from 'commander';
139
+ * import { configure } from './configure';
140
+ *
141
+ * const program = new Command();
142
+ * const configuredProgram = await configure(program, options);
143
+ *
144
+ * // Now the program accepts: --config-directory <path>
145
+ * ```
146
+ */ const configure = async (command, options, _testThrowNonArgumentError)=>{
147
+ // Validate the command object
148
+ if (!command) {
149
+ throw new ArgumentError('command', 'Command instance is required');
150
+ }
151
+ if (typeof command.option !== 'function') {
152
+ throw new ArgumentError('command', 'Command must be a valid Commander.js Command instance');
153
+ }
154
+ // Validate options
155
+ if (!options) {
156
+ throw new ArgumentError('options', 'Options object is required');
157
+ }
158
+ if (!options.defaults) {
159
+ throw new ArgumentError('options.defaults', 'Options must include defaults configuration');
160
+ }
161
+ if (!options.defaults.configDirectory) {
162
+ throw new ArgumentError('options.defaults.configDirectory', 'Default config directory is required');
163
+ }
164
+ // Validate the default config directory
165
+ const validatedDefaultDir = validateConfigDirectory$2(options.defaults.configDirectory);
166
+ let retCommand = command;
167
+ // Add the config directory option with validation
168
+ retCommand = retCommand.option('-c, --config-directory <configDirectory>', 'Configuration directory path', (value)=>{
169
+ try {
170
+ return validateConfigDirectory$2(value, _testThrowNonArgumentError);
171
+ } catch (error) {
172
+ if (error instanceof ArgumentError) {
173
+ // Re-throw with more specific context for CLI usage
174
+ throw new ArgumentError('config-directory', `Invalid --config-directory: ${error.message}`);
175
+ }
176
+ throw error;
177
+ }
178
+ }, validatedDefaultDir);
179
+ return retCommand;
180
+ };
181
+
182
+ /** Default file encoding for reading configuration files */ const DEFAULT_ENCODING = 'utf8';
183
+ /** Default configuration file name to look for in the config directory */ const DEFAULT_CONFIG_FILE = 'config.yaml';
184
+ /**
185
+ * Default configuration options applied when creating a Cardigantime instance.
186
+ * These provide sensible defaults that work for most use cases.
187
+ */ const DEFAULT_OPTIONS = {
188
+ configFile: DEFAULT_CONFIG_FILE,
189
+ isRequired: false,
190
+ encoding: DEFAULT_ENCODING
191
+ };
192
+ /**
193
+ * Default features enabled when creating a Cardigantime instance.
194
+ * Currently includes only the 'config' feature for configuration file support.
195
+ */ const DEFAULT_FEATURES = [
196
+ 'config'
197
+ ];
198
+ /**
199
+ * Default logger implementation using console methods.
200
+ * Provides basic logging functionality when no custom logger is specified.
201
+ * The verbose and silly methods are no-ops to avoid excessive output.
202
+ */ const DEFAULT_LOGGER = {
203
+ // eslint-disable-next-line no-console
204
+ debug: console.debug,
205
+ // eslint-disable-next-line no-console
206
+ info: console.info,
207
+ // eslint-disable-next-line no-console
208
+ warn: console.warn,
209
+ // eslint-disable-next-line no-console
210
+ error: console.error,
211
+ verbose: ()=>{},
212
+ silly: ()=>{}
213
+ };
214
+
215
+ /**
216
+ * Error thrown when file system operations fail
217
+ */ function _define_property$1(obj, key, value) {
218
+ if (key in obj) {
219
+ Object.defineProperty(obj, key, {
220
+ value: value,
221
+ enumerable: true,
222
+ configurable: true,
223
+ writable: true
224
+ });
225
+ } else {
226
+ obj[key] = value;
227
+ }
228
+ return obj;
229
+ }
230
+ class FileSystemError extends Error {
231
+ /**
232
+ * Creates an error for when a required directory doesn't exist
233
+ */ static directoryNotFound(path, isRequired = false) {
234
+ const message = isRequired ? 'Configuration directory does not exist and is required' : 'Configuration directory not found';
235
+ return new FileSystemError('not_found', message, path, 'directory_access');
236
+ }
237
+ /**
238
+ * Creates an error for when a directory exists but isn't readable
239
+ */ static directoryNotReadable(path) {
240
+ const message = 'Configuration directory exists but is not readable';
241
+ return new FileSystemError('not_readable', message, path, 'directory_read');
242
+ }
243
+ /**
244
+ * Creates an error for directory creation failures
245
+ */ static directoryCreationFailed(path, originalError) {
246
+ const message = 'Failed to create directory: ' + (originalError.message || 'Unknown error');
247
+ return new FileSystemError('creation_failed', message, path, 'directory_create', originalError);
248
+ }
249
+ /**
250
+ * Creates an error for file operation failures (glob, etc.)
251
+ */ static operationFailed(operation, path, originalError) {
252
+ const message = `Failed to ${operation}: ${originalError.message || 'Unknown error'}`;
253
+ return new FileSystemError('operation_failed', message, path, operation, originalError);
254
+ }
255
+ /**
256
+ * Creates an error for when a file is not found
257
+ */ static fileNotFound(path) {
258
+ const message = 'Configuration file not found';
259
+ return new FileSystemError('not_found', message, path, 'file_read');
260
+ }
261
+ constructor(errorType, message, path, operation, originalError){
262
+ super(message), _define_property$1(this, "errorType", void 0), _define_property$1(this, "path", void 0), _define_property$1(this, "operation", void 0), _define_property$1(this, "originalError", void 0);
263
+ this.name = 'FileSystemError';
264
+ this.errorType = errorType;
265
+ this.path = path;
266
+ this.operation = operation;
267
+ this.originalError = originalError;
268
+ }
269
+ }
270
+
271
+ // eslint-disable-next-line no-restricted-imports
272
+ const create$1 = (params)=>{
273
+ // eslint-disable-next-line no-console
274
+ const log = params.log || console.log;
275
+ const exists = async (path)=>{
276
+ try {
277
+ await fs__namespace.promises.stat(path);
278
+ return true;
279
+ // eslint-disable-next-line @typescript-eslint/no-unused-vars
280
+ } catch (error) {
281
+ return false;
282
+ }
283
+ };
284
+ const isDirectory = async (path)=>{
285
+ const stats = await fs__namespace.promises.stat(path);
286
+ if (!stats.isDirectory()) {
287
+ log(`${path} is not a directory`);
288
+ return false;
289
+ }
290
+ return true;
291
+ };
292
+ const isFile = async (path)=>{
293
+ const stats = await fs__namespace.promises.stat(path);
294
+ if (!stats.isFile()) {
295
+ log(`${path} is not a file`);
296
+ return false;
297
+ }
298
+ return true;
299
+ };
300
+ const isReadable = async (path)=>{
301
+ try {
302
+ await fs__namespace.promises.access(path, fs__namespace.constants.R_OK);
303
+ } catch (error) {
304
+ log(`${path} is not readable: %s %s`, error.message, error.stack);
305
+ return false;
306
+ }
307
+ return true;
308
+ };
309
+ const isWritable = async (path)=>{
310
+ try {
311
+ await fs__namespace.promises.access(path, fs__namespace.constants.W_OK);
312
+ } catch (error) {
313
+ log(`${path} is not writable: %s %s`, error.message, error.stack);
314
+ return false;
315
+ }
316
+ return true;
317
+ };
318
+ const isFileReadable = async (path)=>{
319
+ return await exists(path) && await isFile(path) && await isReadable(path);
320
+ };
321
+ const isDirectoryWritable = async (path)=>{
322
+ return await exists(path) && await isDirectory(path) && await isWritable(path);
323
+ };
324
+ const isDirectoryReadable = async (path)=>{
325
+ return await exists(path) && await isDirectory(path) && await isReadable(path);
326
+ };
327
+ const createDirectory = async (path)=>{
328
+ try {
329
+ await fs__namespace.promises.mkdir(path, {
330
+ recursive: true
331
+ });
332
+ } catch (mkdirError) {
333
+ throw FileSystemError.directoryCreationFailed(path, mkdirError);
334
+ }
335
+ };
336
+ const readFile = async (path, encoding)=>{
337
+ // Validate encoding parameter
338
+ const validEncodings = [
339
+ 'utf8',
340
+ 'utf-8',
341
+ 'ascii',
342
+ 'latin1',
343
+ 'base64',
344
+ 'hex',
345
+ 'utf16le',
346
+ 'ucs2',
347
+ 'ucs-2'
348
+ ];
349
+ if (!validEncodings.includes(encoding.toLowerCase())) {
350
+ throw new Error('Invalid encoding specified');
351
+ }
352
+ // Check file size before reading to prevent DoS
353
+ try {
354
+ const stats = await fs__namespace.promises.stat(path);
355
+ const maxFileSize = 10 * 1024 * 1024; // 10MB limit
356
+ if (stats.size > maxFileSize) {
357
+ throw new Error('File too large to process');
358
+ }
359
+ } catch (error) {
360
+ if (error.code === 'ENOENT') {
361
+ throw FileSystemError.fileNotFound(path);
362
+ }
363
+ throw error;
364
+ }
365
+ return await fs__namespace.promises.readFile(path, {
366
+ encoding: encoding
367
+ });
368
+ };
369
+ const writeFile = async (path, data, encoding)=>{
370
+ await fs__namespace.promises.writeFile(path, data, {
371
+ encoding: encoding
372
+ });
373
+ };
374
+ const forEachFileIn = async (directory, callback, options = {
375
+ pattern: '*.*'
376
+ })=>{
377
+ try {
378
+ const files = await glob.glob(options.pattern, {
379
+ cwd: directory,
380
+ nodir: true
381
+ });
382
+ for (const file of files){
383
+ await callback(path.join(directory, file));
384
+ }
385
+ } catch (err) {
386
+ throw FileSystemError.operationFailed(`glob pattern ${options.pattern}`, directory, err);
387
+ }
388
+ };
389
+ const readStream = async (path)=>{
390
+ return fs__namespace.createReadStream(path);
391
+ };
392
+ const hashFile = async (path, length)=>{
393
+ const file = await readFile(path, 'utf8');
394
+ return crypto.createHash('sha256').update(file).digest('hex').slice(0, length);
395
+ };
396
+ const listFiles = async (directory)=>{
397
+ return await fs__namespace.promises.readdir(directory);
398
+ };
399
+ return {
400
+ exists,
401
+ isDirectory,
402
+ isFile,
403
+ isReadable,
404
+ isWritable,
405
+ isFileReadable,
406
+ isDirectoryWritable,
407
+ isDirectoryReadable,
408
+ createDirectory,
409
+ readFile,
410
+ readStream,
411
+ writeFile,
412
+ forEachFileIn,
413
+ hashFile,
414
+ listFiles
415
+ };
416
+ };
417
+
418
+ /**
419
+ * Removes undefined values from an object to create a clean configuration.
420
+ * This is used to merge configuration sources while avoiding undefined pollution.
421
+ *
422
+ * @param obj - The object to clean
423
+ * @returns A new object with undefined values filtered out
424
+ */ function clean(obj) {
425
+ return Object.fromEntries(Object.entries(obj).filter(([_, v])=>v !== undefined));
426
+ }
427
+ /**
428
+ * Validates and secures a user-provided path to prevent path traversal attacks.
429
+ *
430
+ * Security checks include:
431
+ * - Path traversal prevention (blocks '..')
432
+ * - Absolute path detection
433
+ * - Path separator validation
434
+ *
435
+ * @param userPath - The user-provided path component
436
+ * @param basePath - The base directory to join the path with
437
+ * @returns The safely joined and normalized path
438
+ * @throws {Error} When path traversal or absolute paths are detected
439
+ */ function validatePath(userPath, basePath) {
440
+ if (!userPath || !basePath) {
441
+ throw new Error('Invalid path parameters');
442
+ }
443
+ const normalized = path__namespace.normalize(userPath);
444
+ // Prevent path traversal attacks
445
+ if (normalized.includes('..') || path__namespace.isAbsolute(normalized)) {
446
+ throw new Error('Invalid path: path traversal detected');
447
+ }
448
+ // Ensure the path doesn't start with a path separator
449
+ if (normalized.startsWith('/') || normalized.startsWith('\\')) {
450
+ throw new Error('Invalid path: absolute path detected');
451
+ }
452
+ return path__namespace.join(basePath, normalized);
453
+ }
454
+ /**
455
+ * Validates a configuration directory path for security and basic formatting.
456
+ *
457
+ * Performs validation to prevent:
458
+ * - Null byte injection attacks
459
+ * - Extremely long paths that could cause DoS
460
+ * - Empty or invalid directory specifications
461
+ *
462
+ * @param configDir - The configuration directory path to validate
463
+ * @returns The normalized configuration directory path
464
+ * @throws {Error} When the directory path is invalid or potentially dangerous
465
+ */ function validateConfigDirectory$1(configDir) {
466
+ if (!configDir) {
467
+ throw new Error('Configuration directory is required');
468
+ }
469
+ // Check for null bytes which could be used for path injection
470
+ if (configDir.includes('\0')) {
471
+ throw new Error('Invalid path: null byte detected');
472
+ }
473
+ const normalized = path__namespace.normalize(configDir);
474
+ // Basic validation - could be expanded based on requirements
475
+ if (normalized.length > 1000) {
476
+ throw new Error('Configuration directory path too long');
477
+ }
478
+ return normalized;
479
+ }
480
+ /**
481
+ * Reads configuration from files and merges it with CLI arguments.
482
+ *
483
+ * This function implements the core configuration loading logic:
484
+ * 1. Validates and resolves the configuration directory path
485
+ * 2. Attempts to read the YAML configuration file
486
+ * 3. Safely parses the YAML content with security protections
487
+ * 4. Merges file configuration with runtime arguments
488
+ * 5. Returns a typed configuration object
489
+ *
490
+ * The function handles missing files gracefully and provides detailed
491
+ * logging for troubleshooting configuration issues.
492
+ *
493
+ * @template T - The Zod schema shape type for configuration validation
494
+ * @param args - Parsed command-line arguments containing potential config overrides
495
+ * @param options - Cardigantime options with defaults, schema, and logger
496
+ * @returns Promise resolving to the merged and typed configuration object
497
+ * @throws {Error} When configuration directory is invalid or required files cannot be read
498
+ *
499
+ * @example
500
+ * ```typescript
501
+ * const config = await read(cliArgs, {
502
+ * defaults: { configDirectory: './config', configFile: 'app.yaml' },
503
+ * configShape: MySchema.shape,
504
+ * logger: console,
505
+ * features: ['config']
506
+ * });
507
+ * // config is fully typed based on your schema
508
+ * ```
509
+ */ const read = async (args, options)=>{
510
+ var _options_defaults;
511
+ const logger = options.logger;
512
+ const storage = create$1({
513
+ log: logger.debug
514
+ });
515
+ const rawConfigDir = args.configDirectory || ((_options_defaults = options.defaults) === null || _options_defaults === void 0 ? void 0 : _options_defaults.configDirectory);
516
+ if (!rawConfigDir) {
517
+ throw new Error('Configuration directory must be specified');
518
+ }
519
+ const resolvedConfigDir = validateConfigDirectory$1(rawConfigDir);
520
+ logger.debug('Resolved config directory');
521
+ const configFile = validatePath(options.defaults.configFile, resolvedConfigDir);
522
+ logger.debug('Attempting to load config file for cardigantime');
523
+ let rawFileConfig = {};
524
+ try {
525
+ const yamlContent = await storage.readFile(configFile, options.defaults.encoding);
526
+ // SECURITY FIX: Use safer parsing options to prevent code execution vulnerabilities
527
+ const parsedYaml = yaml__namespace.load(yamlContent);
528
+ if (parsedYaml !== null && typeof parsedYaml === 'object') {
529
+ rawFileConfig = parsedYaml;
530
+ logger.debug('Loaded configuration file successfully');
531
+ } else if (parsedYaml !== null) {
532
+ logger.warn('Ignoring invalid configuration format. Expected an object, got ' + typeof parsedYaml);
533
+ }
534
+ } catch (error) {
535
+ if (error.code === 'ENOENT' || /not found|no such file/i.test(error.message)) {
536
+ logger.debug('Configuration file not found. Using empty configuration.');
537
+ } else {
538
+ // SECURITY FIX: Don't expose internal paths or detailed error information
539
+ logger.error('Failed to load or parse configuration file: ' + (error.message || 'Unknown error'));
540
+ }
541
+ }
542
+ const config = clean({
543
+ ...rawFileConfig,
544
+ ...{
545
+ configDirectory: resolvedConfigDir
546
+ }
547
+ });
548
+ return config;
549
+ };
550
+
551
+ /**
552
+ * Error thrown when configuration validation fails
553
+ */ function _define_property(obj, key, value) {
554
+ if (key in obj) {
555
+ Object.defineProperty(obj, key, {
556
+ value: value,
557
+ enumerable: true,
558
+ configurable: true,
559
+ writable: true
560
+ });
561
+ } else {
562
+ obj[key] = value;
563
+ }
564
+ return obj;
565
+ }
566
+ class ConfigurationError extends Error {
567
+ /**
568
+ * Creates a validation error for when config doesn't match the schema
569
+ */ static validation(message, zodError, configPath) {
570
+ return new ConfigurationError('validation', message, zodError, configPath);
571
+ }
572
+ /**
573
+ * Creates an error for when extra/unknown keys are found
574
+ */ static extraKeys(extraKeys, allowedKeys, configPath) {
575
+ const message = `Unknown configuration keys found: ${extraKeys.join(', ')}. Allowed keys are: ${allowedKeys.join(', ')}`;
576
+ return new ConfigurationError('extra_keys', message, {
577
+ extraKeys,
578
+ allowedKeys
579
+ }, configPath);
580
+ }
581
+ /**
582
+ * Creates a schema error for when the configuration schema itself is invalid
583
+ */ static schema(message, details) {
584
+ return new ConfigurationError('schema', message, details);
585
+ }
586
+ constructor(errorType, message, details, configPath){
587
+ super(message), _define_property(this, "errorType", void 0), _define_property(this, "details", void 0), _define_property(this, "configPath", void 0);
588
+ this.name = 'ConfigurationError';
589
+ this.errorType = errorType;
590
+ this.details = details;
591
+ this.configPath = configPath;
592
+ }
593
+ }
594
+
595
+ /**
596
+ * Base Zod schema for core Cardigantime configuration.
597
+ * Contains the minimum required configuration fields.
598
+ */ const ConfigSchema = zod.z.object({
599
+ /** The resolved configuration directory path */ configDirectory: zod.z.string()
600
+ });
601
+
602
+ /**
603
+ * Recursively extracts all keys from a Zod schema in dot notation.
604
+ *
605
+ * This function traverses a Zod schema structure and builds a flat list
606
+ * of all possible keys, using dot notation for nested objects. It handles
607
+ * optional/nullable types by unwrapping them and supports arrays by
608
+ * introspecting their element type.
609
+ *
610
+ * Special handling for:
611
+ * - ZodOptional/ZodNullable: Unwraps to get the underlying type
612
+ * - ZodAny/ZodRecord: Accepts any keys, returns the prefix or empty array
613
+ * - ZodArray: Introspects the element type
614
+ * - ZodObject: Recursively processes all shape properties
615
+ *
616
+ * @param schema - The Zod schema to introspect
617
+ * @param prefix - Internal parameter for building nested key paths
618
+ * @returns Array of strings representing all possible keys in dot notation
619
+ *
620
+ * @example
621
+ * ```typescript
622
+ * const schema = z.object({
623
+ * user: z.object({
624
+ * name: z.string(),
625
+ * settings: z.object({ theme: z.string() })
626
+ * }),
627
+ * debug: z.boolean()
628
+ * });
629
+ *
630
+ * const keys = listZodKeys(schema);
631
+ * // Returns: ['user.name', 'user.settings.theme', 'debug']
632
+ * ```
633
+ */ const listZodKeys = (schema, prefix = '')=>{
634
+ // Check if schema has unwrap method (which both ZodOptional and ZodNullable have)
635
+ if (schema._def && (schema._def.typeName === 'ZodOptional' || schema._def.typeName === 'ZodNullable')) {
636
+ // Use type assertion to handle the unwrap method
637
+ const unwrappable = schema;
638
+ return listZodKeys(unwrappable.unwrap(), prefix);
639
+ }
640
+ // Handle ZodAny and ZodRecord - these accept any keys, so don't introspect
641
+ if (schema._def && (schema._def.typeName === 'ZodAny' || schema._def.typeName === 'ZodRecord')) {
642
+ return prefix ? [
643
+ prefix
644
+ ] : [];
645
+ }
646
+ if (schema._def && schema._def.typeName === 'ZodArray') {
647
+ // Use type assertion to handle the element property
648
+ const arraySchema = schema;
649
+ return listZodKeys(arraySchema.element, prefix);
650
+ }
651
+ if (schema._def && schema._def.typeName === 'ZodObject') {
652
+ // Use type assertion to handle the shape property
653
+ const objectSchema = schema;
654
+ return Object.entries(objectSchema.shape).flatMap(([key, subschema])=>{
655
+ const fullKey = prefix ? `${prefix}.${key}` : key;
656
+ const nested = listZodKeys(subschema, fullKey);
657
+ return nested.length ? nested : fullKey;
658
+ });
659
+ }
660
+ return [];
661
+ };
662
+ /**
663
+ * Type guard to check if a value is a plain object (not array, null, or other types).
664
+ *
665
+ * @param value - The value to check
666
+ * @returns True if the value is a plain object
667
+ */ const isPlainObject = (value)=>{
668
+ // Check if it's an object, not null, and not an array.
669
+ return value !== null && typeof value === 'object' && !Array.isArray(value);
670
+ };
671
+ /**
672
+ * Generates a list of all keys within a JavaScript object, using dot notation for nested keys.
673
+ * Mimics the behavior of listZodKeys but operates on plain objects.
674
+ * For arrays, it inspects the first element that is a plain object to determine nested keys.
675
+ * If an array contains no plain objects, or is empty, the key for the array itself is listed.
676
+ *
677
+ * @param obj The object to introspect.
678
+ * @param prefix Internal use for recursion: the prefix for the current nesting level.
679
+ * @returns An array of strings representing all keys in dot notation.
680
+ */ const listObjectKeys = (obj, prefix = '')=>{
681
+ const keys = new Set(); // Use Set to automatically handle duplicates from array recursion
682
+ for(const key in obj){
683
+ // Ensure it's an own property, not from the prototype chain
684
+ if (Object.prototype.hasOwnProperty.call(obj, key)) {
685
+ const value = obj[key];
686
+ const fullKey = prefix ? `${prefix}.${key}` : key;
687
+ if (Array.isArray(value)) {
688
+ // Find the first element that is a plain object to determine structure
689
+ const firstObjectElement = value.find(isPlainObject);
690
+ if (firstObjectElement) {
691
+ // Recurse into the structure of the first object element found
692
+ const nestedKeys = listObjectKeys(firstObjectElement, fullKey);
693
+ nestedKeys.forEach((k)=>keys.add(k));
694
+ } else {
695
+ // Array is empty or contains no plain objects, list the array key itself
696
+ keys.add(fullKey);
697
+ }
698
+ } else if (isPlainObject(value)) {
699
+ // Recurse into nested plain objects
700
+ const nestedKeys = listObjectKeys(value, fullKey);
701
+ nestedKeys.forEach((k)=>keys.add(k));
702
+ } else {
703
+ // It's a primitive, null, or other non-plain object/array type
704
+ keys.add(fullKey);
705
+ }
706
+ }
707
+ }
708
+ return Array.from(keys); // Convert Set back to Array
709
+ };
710
+ /**
711
+ * Validates that the configuration object contains only keys allowed by the schema.
712
+ *
713
+ * This function prevents configuration errors by detecting typos or extra keys
714
+ * that aren't defined in the Zod schema. It intelligently handles:
715
+ * - ZodRecord types that accept arbitrary keys
716
+ * - Nested objects and their key structures
717
+ * - Arrays and their element key structures
718
+ *
719
+ * The function throws a ConfigurationError if extra keys are found, providing
720
+ * helpful information about what keys are allowed vs. what was found.
721
+ *
722
+ * @param mergedSources - The merged configuration object to validate
723
+ * @param fullSchema - The complete Zod schema including base and user schemas
724
+ * @param logger - Logger for error reporting
725
+ * @throws {ConfigurationError} When extra keys are found that aren't in the schema
726
+ *
727
+ * @example
728
+ * ```typescript
729
+ * const schema = z.object({ name: z.string(), age: z.number() });
730
+ * const config = { name: 'John', age: 30, typo: 'invalid' };
731
+ *
732
+ * checkForExtraKeys(config, schema, console);
733
+ * // Throws: ConfigurationError with details about 'typo' being an extra key
734
+ * ```
735
+ */ const checkForExtraKeys = (mergedSources, fullSchema, logger)=>{
736
+ const allowedKeys = new Set(listZodKeys(fullSchema));
737
+ const actualKeys = listObjectKeys(mergedSources);
738
+ // Filter out keys that are under a record type (ZodRecord accepts any keys)
739
+ const recordPrefixes = new Set();
740
+ // Find all prefixes that are ZodRecord types
741
+ const findRecordPrefixes = (schema, prefix = '')=>{
742
+ if (schema._def && (schema._def.typeName === 'ZodOptional' || schema._def.typeName === 'ZodNullable')) {
743
+ const unwrappable = schema;
744
+ findRecordPrefixes(unwrappable.unwrap(), prefix);
745
+ return;
746
+ }
747
+ if (schema._def && (schema._def.typeName === 'ZodAny' || schema._def.typeName === 'ZodRecord')) {
748
+ if (prefix) recordPrefixes.add(prefix);
749
+ return;
750
+ }
751
+ if (schema._def && schema._def.typeName === 'ZodObject') {
752
+ const objectSchema = schema;
753
+ Object.entries(objectSchema.shape).forEach(([key, subschema])=>{
754
+ const fullKey = prefix ? `${prefix}.${key}` : key;
755
+ findRecordPrefixes(subschema, fullKey);
756
+ });
757
+ }
758
+ };
759
+ findRecordPrefixes(fullSchema);
760
+ // Filter out keys that are under record prefixes
761
+ const extraKeys = actualKeys.filter((key)=>{
762
+ if (allowedKeys.has(key)) return false;
763
+ // Check if this key is under a record prefix
764
+ for (const recordPrefix of recordPrefixes){
765
+ if (key.startsWith(recordPrefix + '.')) {
766
+ return false; // This key is allowed under a record
767
+ }
768
+ }
769
+ return true; // This is an extra key
770
+ });
771
+ if (extraKeys.length > 0) {
772
+ const allowedKeysArray = Array.from(allowedKeys);
773
+ const error = ConfigurationError.extraKeys(extraKeys, allowedKeysArray);
774
+ logger.error(error.message);
775
+ throw error;
776
+ }
777
+ };
778
+ /**
779
+ * Validates that a configuration directory exists and is accessible.
780
+ *
781
+ * This function performs file system checks to ensure the configuration
782
+ * directory can be used. It handles the isRequired flag to determine
783
+ * whether a missing directory should cause an error or be silently ignored.
784
+ *
785
+ * @param configDirectory - Path to the configuration directory
786
+ * @param isRequired - Whether the directory must exist
787
+ * @param logger - Optional logger for debug information
788
+ * @throws {FileSystemError} When the directory is required but missing or unreadable
789
+ */ const validateConfigDirectory = async (configDirectory, isRequired, logger)=>{
790
+ const storage = create$1({
791
+ log: (logger === null || logger === void 0 ? void 0 : logger.debug) || (()=>{})
792
+ });
793
+ const exists = await storage.exists(configDirectory);
794
+ if (!exists) {
795
+ if (isRequired) {
796
+ throw FileSystemError.directoryNotFound(configDirectory, true);
797
+ }
798
+ } else if (exists) {
799
+ const isReadable = await storage.isDirectoryReadable(configDirectory);
800
+ if (!isReadable) {
801
+ throw FileSystemError.directoryNotReadable(configDirectory);
802
+ }
803
+ }
804
+ };
805
+ /**
806
+ * Validates a configuration object against the combined Zod schema.
807
+ *
808
+ * This is the main validation function that:
809
+ * 1. Validates the configuration directory (if config feature enabled)
810
+ * 2. Combines the base ConfigSchema with user-provided schema shape
811
+ * 3. Checks for extra keys not defined in the schema
812
+ * 4. Validates all values against their schema definitions
813
+ * 5. Provides detailed error reporting for validation failures
814
+ *
815
+ * The validation is comprehensive and catches common configuration errors
816
+ * including typos, missing required fields, wrong types, and invalid values.
817
+ *
818
+ * @template T - The Zod schema shape type for configuration validation
819
+ * @param config - The merged configuration object to validate
820
+ * @param options - Cardigantime options containing schema, defaults, and logger
821
+ * @throws {ConfigurationError} When configuration validation fails
822
+ * @throws {FileSystemError} When configuration directory validation fails
823
+ *
824
+ * @example
825
+ * ```typescript
826
+ * const schema = z.object({
827
+ * apiKey: z.string().min(1),
828
+ * timeout: z.number().positive(),
829
+ * });
830
+ *
831
+ * await validate(config, {
832
+ * configShape: schema.shape,
833
+ * defaults: { configDirectory: './config', isRequired: true },
834
+ * logger: console,
835
+ * features: ['config']
836
+ * });
837
+ * // Throws detailed errors if validation fails
838
+ * ```
839
+ */ const validate = async (config, options)=>{
840
+ const logger = options.logger;
841
+ if (options.features.includes('config') && config.configDirectory) {
842
+ await validateConfigDirectory(config.configDirectory, options.defaults.isRequired, logger);
843
+ }
844
+ // Combine the base schema with the user-provided shape
845
+ const fullSchema = zod.z.object({
846
+ ...ConfigSchema.shape,
847
+ ...options.configShape
848
+ });
849
+ // Validate the merged sources against the full schema
850
+ const validationResult = fullSchema.safeParse(config);
851
+ // Check for extraneous keys
852
+ checkForExtraKeys(config, fullSchema, logger);
853
+ if (!validationResult.success) {
854
+ const formattedError = JSON.stringify(validationResult.error.format(), null, 2);
855
+ logger.error('Configuration validation failed: %s', formattedError);
856
+ throw ConfigurationError.validation('Configuration validation failed. Check logs for details.', validationResult.error);
857
+ }
858
+ return;
859
+ };
860
+
861
+ /**
862
+ * Creates a new Cardigantime instance for configuration management.
863
+ *
864
+ * Cardigantime handles the complete configuration lifecycle including:
865
+ * - Reading configuration from YAML files
866
+ * - Validating configuration against Zod schemas
867
+ * - Merging CLI arguments with file configuration and defaults
868
+ * - Providing type-safe configuration objects
869
+ *
870
+ * @template T - The Zod schema shape type for your configuration
871
+ * @param pOptions - Configuration options for the Cardigantime instance
872
+ * @param pOptions.defaults - Default configuration settings
873
+ * @param pOptions.defaults.configDirectory - Directory to search for configuration files (required)
874
+ * @param pOptions.defaults.configFile - Name of the configuration file (optional, defaults to 'config.yaml')
875
+ * @param pOptions.defaults.isRequired - Whether the config directory must exist (optional, defaults to false)
876
+ * @param pOptions.defaults.encoding - File encoding for reading config files (optional, defaults to 'utf8')
877
+ * @param pOptions.features - Array of features to enable (optional, defaults to ['config'])
878
+ * @param pOptions.configShape - Zod schema shape defining your configuration structure (required)
879
+ * @param pOptions.logger - Custom logger implementation (optional, defaults to console logger)
880
+ * @returns A Cardigantime instance with methods for configure, read, validate, and setLogger
881
+ *
882
+ * @example
883
+ * ```typescript
884
+ * import { create } from '@theunwalked/cardigantime';
885
+ * import { z } from 'zod';
886
+ *
887
+ * const MyConfigSchema = z.object({
888
+ * apiKey: z.string().min(1),
889
+ * timeout: z.number().default(5000),
890
+ * debug: z.boolean().default(false),
891
+ * });
892
+ *
893
+ * const cardigantime = create({
894
+ * defaults: {
895
+ * configDirectory: './config',
896
+ * configFile: 'myapp.yaml',
897
+ * },
898
+ * configShape: MyConfigSchema.shape,
899
+ * });
900
+ * ```
901
+ */ const create = (pOptions)=>{
13
902
  const defaults = {
14
- ...constants.DEFAULT_OPTIONS,
903
+ ...DEFAULT_OPTIONS,
15
904
  ...pOptions.defaults
16
905
  };
17
- const features = pOptions.features || constants.DEFAULT_FEATURES;
906
+ const features = pOptions.features || DEFAULT_FEATURES;
18
907
  const configShape = pOptions.configShape;
19
- let logger = pOptions.logger || constants.DEFAULT_LOGGER;
908
+ let logger = pOptions.logger || DEFAULT_LOGGER;
20
909
  const options = {
21
910
  defaults,
22
911
  features,
@@ -29,12 +918,15 @@ const create = (pOptions)=>{
29
918
  };
30
919
  return {
31
920
  setLogger,
32
- configure: (command)=>configure.configure(command, options),
33
- validate: (config)=>validate.validate(config, options),
34
- read: (args)=>read.read(args, options)
921
+ configure: (command)=>configure(command, options),
922
+ validate: (config)=>validate(config, options),
923
+ read: (args)=>read(args, options)
35
924
  };
36
925
  };
37
926
 
38
- exports.ConfigSchema = types.ConfigSchema;
927
+ exports.ArgumentError = ArgumentError;
928
+ exports.ConfigSchema = ConfigSchema;
929
+ exports.ConfigurationError = ConfigurationError;
930
+ exports.FileSystemError = FileSystemError;
39
931
  exports.create = create;
40
932
  //# sourceMappingURL=cardigantime.cjs.map