configre 2.0.0 → 2.1.0
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 +13 -7
- package/demo/secrets/demo.js +1 -1
- package/index.js +5 -12
- package/package.json +1 -1
- package/secrets/index.js +7 -15
- package/test/secrets.test.js +57 -26
package/README.md
CHANGED
|
@@ -119,10 +119,10 @@ import Configre from "../../index.js";
|
|
|
119
119
|
import { join } from "node:path";
|
|
120
120
|
|
|
121
121
|
const configPath = join(import.meta.dirname, "config");
|
|
122
|
-
const cfg = Configre(configPath
|
|
122
|
+
const cfg = Configre(configPath);
|
|
123
123
|
```
|
|
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
127
|
> The demo reports `API key configured:` without printing the key. Avoid logging `cfg` in your application: it contains the decrypted secrets.
|
|
128
128
|
|
|
@@ -138,18 +138,24 @@ module.exports = {
|
|
|
138
138
|
};
|
|
139
139
|
```
|
|
140
140
|
|
|
141
|
+
Create `demo/secrets/config/index.secret.cjs` yourself to enable secrets, initially with:
|
|
142
|
+
|
|
143
|
+
```javascript
|
|
144
|
+
module.exports = {};
|
|
145
|
+
```
|
|
146
|
+
|
|
141
147
|
Then run:
|
|
142
148
|
|
|
143
149
|
```bash
|
|
144
150
|
node demo/secrets/demo.js
|
|
145
151
|
```
|
|
146
152
|
|
|
147
|
-
For a new setup, Configre
|
|
153
|
+
For a new setup, Configre generates the encrypted file, recipients directory and Git exclusions alongside your existing configuration files:
|
|
148
154
|
|
|
149
155
|
```text
|
|
150
156
|
demo/secrets/config/
|
|
151
157
|
index.cjs # Public configuration
|
|
152
|
-
index.secret.cjs # Editable secrets
|
|
158
|
+
index.secret.cjs # Editable secrets you create yourself
|
|
153
159
|
recipients/ # Public keys of servers that register
|
|
154
160
|
secrets.enc.json # Generated encrypted values
|
|
155
161
|
.gitignore # Keeps .secret.cjs files out of Git
|
|
@@ -163,7 +169,7 @@ With the public placeholder empty and no secrets added, look for:
|
|
|
163
169
|
API key configured: false
|
|
164
170
|
```
|
|
165
171
|
|
|
166
|
-
|
|
172
|
+
**Configre never creates `.secret.cjs` files.** Without a secret counterpart for the base configuration or selected profile, and without an encrypted file, it loads only public settings and creates no identity or secret artifacts. A secret module contains only the fields you choose to override; public settings continue to come from `index.cjs`.
|
|
167
173
|
|
|
168
174
|
These initialization steps describe a new setup. If `secrets.enc.json` already exists, Configre uses that encrypted file instead of resetting it. A checkout containing the encrypted file but no `.secret.cjs` files is treated as a server checkout.
|
|
169
175
|
|
|
@@ -254,7 +260,7 @@ node demo/secrets/demo.js
|
|
|
254
260
|
|
|
255
261
|
Commit and push the updated `secrets.enc.json`, then pull and restart the demo on the server. **You do not need to copy or register `truco.pub` again.** Its existing authorization also covers new keys you add later.
|
|
256
262
|
|
|
257
|
-
For settings specific to `truco`, add `demo/secrets/config/truco.cjs
|
|
263
|
+
For settings specific to `truco`, add `demo/secrets/config/truco.cjs` and create `truco.secret.cjs` yourself with that profile's secrets. The secret file activates secrets when `truco` is selected, even without `index.secret.cjs`. A public profile alone never creates a secret counterpart. Once secrets are active, all profile secret files, including inactive ones, are encrypted together when the administrator runs the demo.
|
|
258
264
|
|
|
259
265
|
For `--config=truco`, the merge order is:
|
|
260
266
|
|
|
@@ -271,7 +277,7 @@ To revoke this server, remove `demo/secrets/config/recipients/truco.pub`, run th
|
|
|
271
277
|
|
|
272
278
|
- Secret `.cjs` modules execute on the administrator and are reloaded on each Configre call. They must export plain objects containing JSON-compatible objects, arrays, strings, finite numbers, booleans and `null`. Functions, `undefined`, accessors, symbols, custom objects, circular references, and properties named `__proto__`, `constructor` or `prototype` are rejected. Consumers decrypt data without executing these modules.
|
|
273
279
|
- Empty strings stay empty strings. Configre does not fill them from environment variables or populate `process.env`. The existing deep-merge behavior, including array merging, also applies to secrets.
|
|
274
|
-
-
|
|
280
|
+
- Activation depends only on the existing files; there is no `secrets` option. The same behavior applies to `new Configre(configPath).get()` and CommonJS consumers.
|
|
275
281
|
- Each OS user has one identity reused across projects; authorization is per project. A service running under another OS user needs its own registration. All authorized identities can decrypt all profiles in the project's encrypted file.
|
|
276
282
|
- Public recipient files must contain a single RSA-3072 public key in PEM format, with exponent 65537, as generated by Configre. Other file extensions are ignored, and duplicate keys do not add recipients. The administrator is always included.
|
|
277
283
|
- Registration uses an isolated Git index and publishes only the public-key file. It preserves unrelated staged and unstaged changes, runs no commit or pre-push hooks, and does not push local tags, merge, rebase or force-push. Failed registrations can be retried at the next startup; each Git command has a 30-second timeout. An authorized server loads secrets without Git commands or project writes.
|
package/demo/secrets/demo.js
CHANGED
|
@@ -2,7 +2,7 @@ import Configre from "../../index.js";
|
|
|
2
2
|
import { join } from "node:path";
|
|
3
3
|
|
|
4
4
|
const configPath = join(import.meta.dirname, "config");
|
|
5
|
-
const cfg = Configre(configPath
|
|
5
|
+
const cfg = Configre(configPath);
|
|
6
6
|
console.info("API key configured:", Boolean(cfg.api.key));
|
|
7
7
|
console.info(`Set api.key in ${join(configPath, "index.secret.cjs")} and run this demo again.`);
|
|
8
8
|
console.info(`New servers automatically publish their public key in ${join(configPath, "recipients")}. Pull and rerun here to authorize them.`);
|
package/index.js
CHANGED
|
@@ -10,15 +10,10 @@ const requireConfig = createRequire(import.meta.url);
|
|
|
10
10
|
const log = lemonlog("Configre");
|
|
11
11
|
|
|
12
12
|
class ConfigreClass {
|
|
13
|
-
constructor(pathOrDir
|
|
13
|
+
constructor(pathOrDir) {
|
|
14
14
|
if (typeof pathOrDir !== "string" || pathOrDir.length === 0) {
|
|
15
15
|
throw new TypeError("Configre path must be a non-empty string");
|
|
16
16
|
}
|
|
17
|
-
if (options === null || typeof options !== "object" || Array.isArray(options) ||
|
|
18
|
-
(options.secrets !== undefined && typeof options.secrets !== "boolean")) {
|
|
19
|
-
throw new TypeError("Configre options must be an object with an optional boolean secrets property");
|
|
20
|
-
}
|
|
21
|
-
|
|
22
17
|
const dir = path.join(pathOrDir);
|
|
23
18
|
const isNested = (ConfigreClass._nesting || 0) > 0;
|
|
24
19
|
ConfigreClass._nesting = (ConfigreClass._nesting || 0) + 1;
|
|
@@ -35,9 +30,7 @@ class ConfigreClass {
|
|
|
35
30
|
const configArg = process.argv.find(arg => arg.startsWith('--config='));
|
|
36
31
|
this.profile = configArg ? configArg.slice('--config='.length) : os.hostname();
|
|
37
32
|
this.profileSettings = this.loadProfileSettings();
|
|
38
|
-
this.secretSettings =
|
|
39
|
-
? loadSecrets(pathOrDir, this.configFile, this.profile)
|
|
40
|
-
: [];
|
|
33
|
+
this.secretSettings = loadSecrets(pathOrDir, this.configFile, this.profile);
|
|
41
34
|
} finally {
|
|
42
35
|
ConfigreClass._nesting -= 1;
|
|
43
36
|
}
|
|
@@ -93,11 +86,11 @@ class ConfigreClass {
|
|
|
93
86
|
}
|
|
94
87
|
|
|
95
88
|
// Wrapper function to support both constructor and function usage
|
|
96
|
-
function Configre(path
|
|
89
|
+
function Configre(path) {
|
|
97
90
|
if (this instanceof Configre) {
|
|
98
|
-
return new ConfigreClass(path
|
|
91
|
+
return new ConfigreClass(path);
|
|
99
92
|
} else {
|
|
100
|
-
return new ConfigreClass(path
|
|
93
|
+
return new ConfigreClass(path).get();
|
|
101
94
|
}
|
|
102
95
|
}
|
|
103
96
|
|
package/package.json
CHANGED
package/secrets/index.js
CHANGED
|
@@ -122,8 +122,14 @@ function selectSecrets(bundle, paths, profile) {
|
|
|
122
122
|
|
|
123
123
|
function loadSecrets(configPath, configFile, profile) {
|
|
124
124
|
const paths = secretPaths(configPath, configFile);
|
|
125
|
+
const names = localFiles(paths);
|
|
126
|
+
const hasEncrypted = !!stat(paths.encrypted);
|
|
127
|
+
const hasLocal = names.includes(path.basename(paths.local)) || (paths.directory &&
|
|
128
|
+
(names.includes(profile + ".dev.secret.cjs") || names.includes(profile + ".secret.cjs")));
|
|
129
|
+
if (!hasLocal && !hasEncrypted) return [];
|
|
130
|
+
|
|
125
131
|
const identity = loadIdentity();
|
|
126
|
-
if (
|
|
132
|
+
if (names.length === 0 && hasEncrypted) {
|
|
127
133
|
try {
|
|
128
134
|
return selectSecrets(validateBundle(decrypt(readJSON(paths.encrypted), identity)), paths, profile);
|
|
129
135
|
} catch (error) {
|
|
@@ -143,20 +149,6 @@ function loadSecrets(configPath, configFile, profile) {
|
|
|
143
149
|
prepareIgnore(paths);
|
|
144
150
|
const settings = { files: Object.fromEntries(names.map(name => [name, readSecret(path.join(paths.parent, name))])) };
|
|
145
151
|
const recipients = loadRecipients(paths.recipients, identity);
|
|
146
|
-
const templates = new Set([path.basename(paths.local)]);
|
|
147
|
-
if (paths.directory) {
|
|
148
|
-
for (const name of fs.readdirSync(paths.parent)) {
|
|
149
|
-
if (name.endsWith(".cjs") && !name.endsWith(".secret.cjs") && stat(path.join(paths.parent, name)).isFile()) {
|
|
150
|
-
templates.add(name.slice(0, -4) + ".secret.cjs");
|
|
151
|
-
}
|
|
152
|
-
}
|
|
153
|
-
}
|
|
154
|
-
for (const name of templates) {
|
|
155
|
-
if (!Object.hasOwn(settings.files, name)) {
|
|
156
|
-
fs.writeFileSync(path.join(paths.parent, name), "module.exports = {};\n", { flag: "wx", mode: 0o600 });
|
|
157
|
-
settings.files[name] = {};
|
|
158
|
-
}
|
|
159
|
-
}
|
|
160
152
|
if (hasEncrypted) {
|
|
161
153
|
const previousRecipients = envelope.recipients.map(({ fingerprint, publicKey }) => ({ fingerprint, publicKey }));
|
|
162
154
|
if (isDeepStrictEqual(settings, previous) && isDeepStrictEqual(recipients, previousRecipients)) {
|
package/test/secrets.test.js
CHANGED
|
@@ -51,7 +51,7 @@ function writeSettings(f, settings = { api: { key: sentinel } }) {
|
|
|
51
51
|
}
|
|
52
52
|
|
|
53
53
|
function load(f) {
|
|
54
|
-
return Configre(f.config
|
|
54
|
+
return Configre(f.config);
|
|
55
55
|
}
|
|
56
56
|
|
|
57
57
|
function envelope(f) {
|
|
@@ -98,23 +98,40 @@ function gitFixture(t) {
|
|
|
98
98
|
};
|
|
99
99
|
}
|
|
100
100
|
|
|
101
|
-
test("
|
|
101
|
+
test("missing secret counterparts leave configuration and identity untouched", t => {
|
|
102
102
|
const f = fixture(t, { seed: false });
|
|
103
|
-
fs.
|
|
103
|
+
const publicFiles = fs.readdirSync(f.config);
|
|
104
104
|
t.mock.method(os, "homedir", () => { throw new Error("must not access the identity"); });
|
|
105
105
|
assert.equal(Configre(f.config).api.key, "");
|
|
106
|
-
assert.
|
|
106
|
+
assert.deepEqual(fs.readdirSync(f.config), publicFiles);
|
|
107
|
+
fs.writeFileSync(path.join(f.config, "other.secret.cjs"), "invalid-json");
|
|
108
|
+
const files = fs.readdirSync(f.config);
|
|
109
|
+
assert.equal(Configre(f.config).api.key, "");
|
|
107
110
|
assert.equal(new Configre(f.config).get().api.key, "");
|
|
108
|
-
|
|
109
|
-
assert.throws(() => Configre(f.config, options), /options must be an object/);
|
|
110
|
-
}
|
|
111
|
-
assert.equal(fs.existsSync(f.encrypted), false);
|
|
112
|
-
assert.equal(fs.existsSync(path.join(f.config, ".gitignore")), false);
|
|
111
|
+
assert.deepEqual(fs.readdirSync(f.config), files);
|
|
113
112
|
assert.equal(fs.existsSync(f.homes[0]), false);
|
|
114
113
|
});
|
|
115
114
|
|
|
116
|
-
|
|
115
|
+
for (const profile of ["testhost", "testhost.dev", "forced"]) {
|
|
116
|
+
test(`a ${profile} secret activates secrets without a base secret module`, t => {
|
|
117
|
+
const f = fixture(t);
|
|
118
|
+
if (profile === "forced") {
|
|
119
|
+
const previousArgs = process.argv;
|
|
120
|
+
process.argv = [...previousArgs.filter(arg => !arg.startsWith("--config=")), "--config=forced"];
|
|
121
|
+
t.after(() => { process.argv = previousArgs; });
|
|
122
|
+
}
|
|
123
|
+
const local = path.join(f.config, profile + ".secret.cjs");
|
|
124
|
+
writeSettings({ local });
|
|
125
|
+
assert.equal(load(f).api.key, sentinel);
|
|
126
|
+
assert.equal(fs.existsSync(f.local), false);
|
|
127
|
+
assert.deepEqual(fs.readdirSync(f.config).filter(name => name.endsWith(".secret.cjs")), [path.basename(local)]);
|
|
128
|
+
assert.equal(load(consumerCopy(f, "profile-only-consumer")).api.key, sentinel);
|
|
129
|
+
});
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
test("an existing empty secret module initializes secrets and reuses generated files", t => {
|
|
117
133
|
const f = fixture(t, { seed: false });
|
|
134
|
+
fs.writeFileSync(f.local, "module.exports = {};\n", { mode: 0o600 });
|
|
118
135
|
const ignore = path.join(f.config, ".gitignore");
|
|
119
136
|
fs.writeFileSync(ignore, "# existing rules\n*.log");
|
|
120
137
|
const config = load(f);
|
|
@@ -198,7 +215,7 @@ test("secrets merge last with profiles, arrays, JSON values and the constructor
|
|
|
198
215
|
assert.deepEqual(config.api, settings.api);
|
|
199
216
|
assert.deepEqual(config.list, [9, 2]);
|
|
200
217
|
assert.deepEqual(config.json, settings.json);
|
|
201
|
-
assert.deepEqual(new Configre(f.config
|
|
218
|
+
assert.deepEqual(new Configre(f.config).get(), config);
|
|
202
219
|
assert.equal(fs.existsSync(f.recipients), true);
|
|
203
220
|
assert.equal(fs.readFileSync(f.encrypted, "utf8").includes(sentinel), false);
|
|
204
221
|
assert.equal(envelope(f).recipients.length, 1);
|
|
@@ -277,19 +294,19 @@ test("base and host secrets select dev and forced profiles identically on consum
|
|
|
277
294
|
assert.deepEqual(fs.readdirSync(consumer.config), consumerFiles);
|
|
278
295
|
});
|
|
279
296
|
|
|
280
|
-
test("administrator
|
|
297
|
+
test("administrator leaves missing counterparts absent and preserves existing host secrets", t => {
|
|
281
298
|
const f = fixture(t);
|
|
282
299
|
writeSettings(f);
|
|
283
300
|
load(f);
|
|
284
301
|
const host = { local: path.join(f.config, "testhost.secret.cjs") };
|
|
285
|
-
assert.equal(fs.
|
|
302
|
+
assert.equal(fs.existsSync(host.local), false);
|
|
286
303
|
writeSettings(host, { api: { key: "host-secret" } });
|
|
287
304
|
const original = fs.readFileSync(host.local);
|
|
288
305
|
fs.writeFileSync(path.join(f.config, "newhost.cjs"), "module.exports = { public: true };\n");
|
|
289
306
|
load(f);
|
|
290
307
|
assert.deepEqual(fs.readFileSync(host.local), original);
|
|
291
|
-
assert.equal(fs.
|
|
292
|
-
const consumer = consumerCopy(f, "
|
|
308
|
+
assert.equal(fs.existsSync(path.join(f.config, "newhost.secret.cjs")), false);
|
|
309
|
+
const consumer = consumerCopy(f, "optional-profile-consumer");
|
|
293
310
|
fs.writeFileSync(path.join(consumer.config, "newhost.cjs"), "module.exports = {};\n");
|
|
294
311
|
assert.equal(load(consumer).api.key, "host-secret");
|
|
295
312
|
assert.equal(fs.existsSync(path.join(consumer.config, "newhost.secret.cjs")), false);
|
|
@@ -467,20 +484,33 @@ test("private permissions and symlinks are rejected", { skip: process.platform =
|
|
|
467
484
|
assert.throws(() => load(f), /regular file/);
|
|
468
485
|
});
|
|
469
486
|
|
|
487
|
+
test("explicit config files without their own secret sidecar leave identities and files untouched", t => {
|
|
488
|
+
const f = fixture(t, { seed: false });
|
|
489
|
+
const filename = path.join(f.root, "settings.cjs");
|
|
490
|
+
fs.writeFileSync(filename, 'module.exports = { public: true };');
|
|
491
|
+
fs.writeFileSync(path.join(f.root, "other.secret.cjs"), "invalid-json");
|
|
492
|
+
const files = fs.readdirSync(f.root);
|
|
493
|
+
t.mock.method(os, "homedir", () => { throw new Error("must not access the identity"); });
|
|
494
|
+
assert.deepEqual(Configre(filename), { public: true });
|
|
495
|
+
assert.deepEqual(Configre(filename.slice(0, -4)), { public: true });
|
|
496
|
+
assert.deepEqual(fs.readdirSync(f.root), files);
|
|
497
|
+
assert.equal(fs.existsSync(f.homes[0]), false);
|
|
498
|
+
});
|
|
499
|
+
|
|
470
500
|
test("explicit config files and extensionless paths use sidecars beside the resolved file", t => {
|
|
471
501
|
const f = fixture(t);
|
|
472
502
|
const filename = path.join(f.root, "settings.cjs");
|
|
473
503
|
fs.writeFileSync(filename, 'module.exports = { api: { key: "", host: "file" } };');
|
|
474
504
|
const local = filename.slice(0, -4) + ".secret.cjs";
|
|
475
505
|
writeSettings({ local });
|
|
476
|
-
assert.equal(Configre(filename
|
|
477
|
-
assert.equal(Configre(filename.slice(0, -4)
|
|
506
|
+
assert.equal(Configre(filename).api.key, sentinel);
|
|
507
|
+
assert.equal(Configre(filename.slice(0, -4)).api.key, sentinel);
|
|
478
508
|
assert.equal(fs.existsSync(filename + ".secrets.enc.json"), true);
|
|
479
509
|
assert.equal(fs.existsSync(filename + ".recipients"), true);
|
|
480
510
|
assert.equal(fs.existsSync(path.join(f.root, "secrets.enc.json")), false);
|
|
481
511
|
const relative = path.relative(process.cwd(), f.config);
|
|
482
512
|
writeSettings(f);
|
|
483
|
-
assert.equal(Configre(relative
|
|
513
|
+
assert.equal(Configre(relative).api.key, sentinel);
|
|
484
514
|
});
|
|
485
515
|
|
|
486
516
|
test("ESM callers can use the same synchronous API", t => {
|
|
@@ -494,7 +524,7 @@ test("ESM callers can use the same synchronous API", t => {
|
|
|
494
524
|
import os from 'node:os';
|
|
495
525
|
import Configre from ${JSON.stringify(moduleURL)};
|
|
496
526
|
os.homedir = () => process.env.CONFIGRE_TEST_HOME;
|
|
497
|
-
const cfg = Configre(process.env.CONFIGRE_TEST_PATH
|
|
527
|
+
const cfg = Configre(process.env.CONFIGRE_TEST_PATH);
|
|
498
528
|
assert.equal(typeof cfg.then, 'undefined');
|
|
499
529
|
assert.equal(cfg.api.key, ${JSON.stringify(sentinel)});
|
|
500
530
|
`);
|
|
@@ -511,7 +541,7 @@ test("symlinked configuration directories keep directory sidecar names", { skip:
|
|
|
511
541
|
writeSettings(f);
|
|
512
542
|
const linked = path.join(f.root, "linked-config");
|
|
513
543
|
fs.symlinkSync(f.config, linked);
|
|
514
|
-
assert.equal(Configre(linked
|
|
544
|
+
assert.equal(Configre(linked).api.key, sentinel);
|
|
515
545
|
assert.equal(fs.existsSync(f.encrypted), true);
|
|
516
546
|
assert.equal(fs.existsSync(path.join(f.config, "index.cjs.secrets.enc.json")), false);
|
|
517
547
|
});
|
|
@@ -520,13 +550,12 @@ test("error output never includes local secret values or private key contents",
|
|
|
520
550
|
const f = fixture(t);
|
|
521
551
|
fs.writeFileSync(f.local, `module.exports = {secret: "${sentinel}", bad`);
|
|
522
552
|
assert.throws(() => load(f), /invalid secret module/);
|
|
523
|
-
assert.equal(new Configre(f.config)._isNested, false);
|
|
524
553
|
const script = `
|
|
525
554
|
const os = require('node:os');
|
|
526
555
|
os.homedir = () => process.env.CONFIGRE_TEST_HOME;
|
|
527
556
|
const Configre = require(process.env.CONFIGRE_TEST_MODULE);
|
|
528
557
|
try {
|
|
529
|
-
Configre(process.env.CONFIGRE_TEST_PATH
|
|
558
|
+
Configre(process.env.CONFIGRE_TEST_PATH);
|
|
530
559
|
} catch (error) {
|
|
531
560
|
console.error(error.stack);
|
|
532
561
|
process.exitCode = 1;
|
|
@@ -545,6 +574,8 @@ test("error output never includes local secret values or private key contents",
|
|
|
545
574
|
assert.match(result.stderr, /invalid secret module/);
|
|
546
575
|
assert.equal((result.stdout + result.stderr).includes(sentinel), false);
|
|
547
576
|
assert.equal((result.stdout + result.stderr).includes("BEGIN PRIVATE KEY"), false);
|
|
577
|
+
writeSettings(f);
|
|
578
|
+
assert.equal(new Configre(f.config)._isNested, false);
|
|
548
579
|
});
|
|
549
580
|
|
|
550
581
|
test("Git exclusions preserve existing rules and never hide already tracked local files", t => {
|
|
@@ -568,6 +599,7 @@ test("Git exclusions preserve existing rules and never hide already tracked loca
|
|
|
568
599
|
assert.deepEqual(fs.readFileSync(f.encrypted), original);
|
|
569
600
|
assert.ok(git(f.root, ["ls-files"]).includes("config/index.secret.cjs"));
|
|
570
601
|
git(f.root, ["rm", "--cached", "config/index.secret.cjs"]);
|
|
602
|
+
writeSettings({ local: path.join(f.config, "testhost.secret.cjs") }, {});
|
|
571
603
|
git(f.root, ["add", "-f", "config/testhost.secret.cjs"]);
|
|
572
604
|
assert.throws(() => load(f), /tracked by Git/);
|
|
573
605
|
});
|
|
@@ -579,13 +611,13 @@ test("recipient files can be tracked and sidecar names are escaped literally in
|
|
|
579
611
|
fs.writeFileSync(filename, "module.exports = {};");
|
|
580
612
|
const local = filename.slice(0, -4) + ".secret.cjs";
|
|
581
613
|
writeSettings({ local });
|
|
582
|
-
Configre(filename
|
|
614
|
+
Configre(filename);
|
|
583
615
|
assert.ok(git(f.root, ["check-ignore", path.basename(local)]).includes(path.basename(local)));
|
|
584
616
|
const recipient = filename + ".recipients/developer.pub";
|
|
585
617
|
fs.copyFileSync(f.publicPath(1), recipient);
|
|
586
618
|
git(f.root, ["add", "-f", path.relative(f.root, recipient)]);
|
|
587
619
|
const original = fs.readFileSync(filename + ".secrets.enc.json");
|
|
588
|
-
assert.equal(Configre(filename
|
|
620
|
+
assert.equal(Configre(filename).api.key, sentinel);
|
|
589
621
|
assert.notDeepEqual(fs.readFileSync(filename + ".secrets.enc.json"), original);
|
|
590
622
|
});
|
|
591
623
|
|
|
@@ -773,7 +805,7 @@ test("a second process cannot write while an encrypted replacement is pending",
|
|
|
773
805
|
const os = require('node:os');
|
|
774
806
|
os.homedir = () => process.env.CONFIGRE_TEST_HOME;
|
|
775
807
|
const Configre = require(process.env.CONFIGRE_TEST_MODULE);
|
|
776
|
-
assert.throws(() => Configre(process.env.CONFIGRE_TEST_PATH
|
|
808
|
+
assert.throws(() => Configre(process.env.CONFIGRE_TEST_PATH), /another writer/);
|
|
777
809
|
`], {
|
|
778
810
|
encoding: "utf8",
|
|
779
811
|
env: {
|
|
@@ -792,6 +824,5 @@ test("a second process cannot write while an encrypted replacement is pending",
|
|
|
792
824
|
assert.equal(load(f).api.key, sentinel + "-new");
|
|
793
825
|
assert.equal(contested, true);
|
|
794
826
|
fs.unlinkSync(f.local);
|
|
795
|
-
fs.unlinkSync(path.join(f.config, "testhost.secret.cjs"));
|
|
796
827
|
assert.equal(load(f).api.key, sentinel + "-new");
|
|
797
828
|
});
|