configre 2.1.2 → 2.1.4

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -124,7 +124,24 @@ const cfg = Configre(configPath);
124
124
 
125
125
  Secrets activate automatically when the base configuration or selected profile has a corresponding `.secret.cjs` file, or when `secrets.enc.json` already exists. No options are needed. Loading remains synchronous, and your application reads the result through ordinary properties such as `cfg.api.key`.
126
126
 
127
- > The demo reports `API key configured:` without printing the key. Avoid logging `cfg` in your application: it contains the decrypted secrets.
127
+ > The demo reports `API key configured:` without printing the key. Avoid logging `cfg` in your application: it contains the decrypted secrets. Use `cfg.print()` to log configuration with secret fields omitted.
128
+
129
+ ```javascript
130
+ const cfg = Configre(configPath);
131
+ cfg.print();
132
+ ```
133
+
134
+ `print()` calls Configre's `log.debug`; enable its output with `DEBUG=Configre:*`.
135
+ It omits fields supplied by the base and selected profile's `.secret.cjs` files,
136
+ including when loaded from the encrypted file. Public siblings in nested objects
137
+ remain visible; arrays supplied by secrets are omitted entirely. It does not
138
+ modify the configuration or log automatically on load. `cfg.print()` logs its
139
+ current values, including changes made after loading. The method is non-enumerable
140
+ and does not appear in `Object.keys(cfg)`, object spreads or JSON output.
141
+ `new Configre(configPath).print()` is also supported. If your configuration already
142
+ has a field named `print`, that value is preserved; use the constructor API to print it.
143
+ Sensitivity is determined by secret-file fields, not by names such as `password`
144
+ or `token`: keep sensitive values in `.secret.cjs` files, not in public settings.
128
145
 
129
146
  ### 1. Start the demo on the administrator machine
130
147
 
@@ -3,6 +3,7 @@ import { join } from "node:path";
3
3
 
4
4
  const configPath = join(import.meta.dirname, "config");
5
5
  const cfg = Configre(configPath);
6
+ cfg.print();
6
7
  console.info("API key configured:", Boolean(cfg.api.key));
7
8
  console.info(`Set api.key in ${join(configPath, "index.secret.cjs")} and run this demo again.`);
8
9
  console.info(`New servers automatically publish their public key in ${join(configPath, "recipients")}. Pull and rerun here to authorize them.`);
package/index.js CHANGED
@@ -9,6 +9,28 @@ import loadSecrets from "./secrets/index.js";
9
9
  const requireConfig = createRequire(import.meta.url);
10
10
  const log = lemonlog("Configre");
11
11
 
12
+ function omitSecrets(settings, secrets) {
13
+ for (const [key, value] of Object.entries(secrets)) {
14
+ if (!Object.hasOwn(settings, key)) continue;
15
+ const current = settings[key];
16
+ if (value !== null && typeof value === "object" && !Array.isArray(value) &&
17
+ current !== null && typeof current === "object" && !Array.isArray(current)) {
18
+ omitSecrets(current, value);
19
+ if (Object.keys(current).length === 0) delete settings[key];
20
+ } else {
21
+ delete settings[key];
22
+ }
23
+ }
24
+ }
25
+
26
+ function printSettings(settings, secretSettings) {
27
+ const output = merge({}, settings);
28
+ for (const secrets of secretSettings) {
29
+ omitSecrets(output, secrets);
30
+ }
31
+ log.debug(output);
32
+ }
33
+
12
34
  class ConfigreClass {
13
35
  constructor(pathOrDir) {
14
36
  if (typeof pathOrDir !== "string" || pathOrDir.length === 0) {
@@ -83,6 +105,10 @@ class ConfigreClass {
83
105
  get() {
84
106
  return merge({}, this.defaultSettings, this.profileSettings, ...this.secretSettings);
85
107
  }
108
+
109
+ print() {
110
+ printSettings(this.get(), this.secretSettings);
111
+ }
86
112
  }
87
113
 
88
114
  // Wrapper function to support both constructor and function usage
@@ -90,7 +116,14 @@ function Configre(path) {
90
116
  if (this instanceof Configre) {
91
117
  return new ConfigreClass(path);
92
118
  } else {
93
- return new ConfigreClass(path).get();
119
+ const config = new ConfigreClass(path);
120
+ const settings = config.get();
121
+ if (!Object.hasOwn(settings, "print")) {
122
+ Object.defineProperty(settings, "print", {
123
+ value: () => printSettings(settings, config.secretSettings)
124
+ });
125
+ }
126
+ return settings;
94
127
  }
95
128
  }
96
129
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "configre",
3
- "version": "2.1.2",
3
+ "version": "2.1.4",
4
4
  "description": "🔧 Effortlessly Tailor Your Settings",
5
5
  "type": "module",
6
6
  "engines": {
@@ -80,6 +80,105 @@ function consumerCopy(f, name) {
80
80
  return { config };
81
81
  }
82
82
 
83
+ test("print logs public configuration without creating secret artifacts", t => {
84
+ const f = fixture(t, { seed: false });
85
+ const calls = [];
86
+ t.mock.method(Object.getPrototypeOf(log), "debug", (...args) => calls.push(args));
87
+ const config = new Configre(f.config);
88
+
89
+ assert.equal(calls.length, 0);
90
+ config.print();
91
+ assert.deepEqual(calls, [[config.get()]]);
92
+ assert.equal(fs.existsSync(f.homes[0]), false);
93
+ });
94
+
95
+ test("function results expose print without changing enumerable data and log current settings", t => {
96
+ const f = fixture(t, { seed: false });
97
+ const calls = [];
98
+ t.mock.method(Object.getPrototypeOf(log), "debug", (...args) => calls.push(args));
99
+ const cfg = Configre(f.config);
100
+ const expected = new Configre(f.config).get();
101
+
102
+ assert.equal(typeof cfg.print, "function");
103
+ assert.deepEqual(Object.keys(cfg), Object.keys(expected));
104
+ assert.deepEqual({ ...cfg }, expected);
105
+ assert.equal(JSON.stringify(cfg), JSON.stringify(expected));
106
+ cfg.api.host = "updated";
107
+ cfg.print();
108
+ assert.deepEqual(calls, [[{ ...expected, api: { key: "", host: "updated" } }]]);
109
+ assert.equal(cfg.api.host, "updated");
110
+ });
111
+
112
+ test("a public print field remains configuration data", t => {
113
+ const f = fixture(t, { seed: false });
114
+ fs.writeFileSync(path.join(f.config, "index.cjs"), 'module.exports = { print: false };');
115
+ assert.equal(Configre(f.config).print, false);
116
+ const calls = [];
117
+ t.mock.method(Object.getPrototypeOf(log), "debug", (...args) => calls.push(args));
118
+ new Configre(f.config).print();
119
+ assert.equal(calls[0][0].print, false);
120
+ });
121
+
122
+ test("print omits local and encrypted secret fields without changing effective settings", t => {
123
+ const f = fixture(t);
124
+ const calls = [];
125
+ t.mock.method(Object.getPrototypeOf(log), "debug", (...args) => calls.push(args));
126
+ writeSettings(f, {
127
+ api: { key: sentinel },
128
+ list: [sentinel],
129
+ privateGroup: { value: sentinel },
130
+ optional: null,
131
+ empty: "",
132
+ enabled: false,
133
+ count: 0
134
+ });
135
+ fs.writeFileSync(path.join(f.config, "testhost.secret.cjs"),
136
+ `module.exports = { api: { other: ${JSON.stringify(sentinel)} }, privateGroup: "public-looking" };`);
137
+
138
+ load(f);
139
+ for (const source of [f, consumerCopy(f, "debug-consumer")]) {
140
+ const config = new Configre(source.config);
141
+ const before = config.get();
142
+ const secretsBefore = structuredClone(config.secretSettings);
143
+ config.print();
144
+
145
+ assert.deepEqual(calls.at(-1), [{ api: { host: "profile" } }]);
146
+ assert.equal(JSON.stringify(calls).includes(sentinel), false);
147
+ assert.deepEqual(config.get(), before);
148
+ assert.deepEqual(config.secretSettings, secretsBefore);
149
+ assert.equal(before.api.key, sentinel);
150
+ assert.equal(before.list[0], sentinel);
151
+ assert.equal(before.list[1], 2);
152
+ const cfg = Configre(source.config);
153
+ cfg.api.key = sentinel + "-updated";
154
+ cfg.api.host = "updated";
155
+ cfg.print();
156
+ assert.deepEqual(calls.at(-1), [{ api: { host: "updated" } }]);
157
+ assert.equal(JSON.stringify(calls).includes(sentinel), false);
158
+ assert.equal(cfg.api.key, sentinel + "-updated");
159
+ }
160
+ const result = spawnSync(process.execPath, ["-e", `
161
+ const os = require('node:os');
162
+ os.homedir = () => process.env.CONFIGRE_TEST_HOME;
163
+ const Configre = require(process.env.CONFIGRE_TEST_MODULE);
164
+ Configre(process.env.CONFIGRE_TEST_PATH).print();
165
+ `, "--", "--config=testhost"], {
166
+ encoding: "utf8",
167
+ env: {
168
+ ...process.env, DEBUG: "Configre:*",
169
+ CONFIGRE_TEST_HOME: f.homes[0],
170
+ CONFIGRE_TEST_MODULE: path.join(import.meta.dirname, "..", "index.js"),
171
+ CONFIGRE_TEST_PATH: path.join(f.root, "debug-consumer")
172
+ }
173
+ });
174
+ assert.equal(result.status, 0, result.stderr);
175
+ const output = result.stdout + result.stderr;
176
+ assert.match(output, /Configre:debug/);
177
+ assert.match(output, /host: 'profile'/);
178
+ assert.equal(output.includes(sentinel), false);
179
+ assert.equal(output.includes("privateGroup"), false);
180
+ });
181
+
83
182
  function git(directory, args) {
84
183
  const result = spawnSync("git", ["-C", directory, ...args], { encoding: "utf8" });
85
184
  assert.equal(result.status, 0, result.stderr);