@mudah-cli/config 0.7.0 → 0.9.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.
@@ -0,0 +1,302 @@
1
+ import { spawnSync } from 'node:child_process';
2
+ import { chmodSync, mkdirSync, readFileSync, writeFileSync } from 'node:fs';
3
+ import { homedir } from 'node:os';
4
+ import { join } from 'node:path';
5
+ import { isPlainObject } from './paths.js';
6
+ import { redactSecrets } from './redact.js';
7
+ const ENV_PREFIX = 'MUDAH_SECRET_';
8
+ function readJsonObject(file) {
9
+ try {
10
+ const parsed = JSON.parse(readFileSync(file, 'utf8'));
11
+ if (!isPlainObject(parsed))
12
+ return {};
13
+ const out = {};
14
+ for (const [key, value] of Object.entries(parsed)) {
15
+ if (typeof value === 'string')
16
+ out[key] = value;
17
+ }
18
+ return out;
19
+ }
20
+ catch {
21
+ return {};
22
+ }
23
+ }
24
+ function writeJsonObject(file, data) {
25
+ mkdirSync(join(file, '..'), { recursive: true });
26
+ writeFileSync(file, `${JSON.stringify(data, null, 2)}\n`, { encoding: 'utf8', mode: 0o600 });
27
+ // mode on writeFileSync is ignored when the file already exists.
28
+ chmodSync(file, 0o600);
29
+ }
30
+ function defaultSecretsDir() {
31
+ return join(homedir(), '.mudah', 'secrets');
32
+ }
33
+ function secretsFile(dir) {
34
+ return dir.endsWith('.json') ? dir : join(dir, 'secrets.json');
35
+ }
36
+ /**
37
+ * Read secrets from the environment. `get('FOO')` looks at
38
+ * `MUDAH_SECRET_FOO`, then `process.env.FOO`.
39
+ */
40
+ export function envSecretDriver(env = process.env) {
41
+ return {
42
+ name: 'env',
43
+ get(name) {
44
+ const prefixed = env[`${ENV_PREFIX}${name}`];
45
+ if (prefixed !== undefined)
46
+ return prefixed;
47
+ const upper = env[`${ENV_PREFIX}${name.toUpperCase()}`];
48
+ if (upper !== undefined)
49
+ return upper;
50
+ return env[name];
51
+ },
52
+ set(name, value) {
53
+ env[`${ENV_PREFIX}${name}`] = value;
54
+ },
55
+ delete(name) {
56
+ const keys = [`${ENV_PREFIX}${name}`, `${ENV_PREFIX}${name.toUpperCase()}`, name];
57
+ let removed = false;
58
+ for (const key of keys) {
59
+ if (env[key] !== undefined) {
60
+ delete env[key];
61
+ removed = true;
62
+ }
63
+ }
64
+ return removed;
65
+ },
66
+ list() {
67
+ const names = [];
68
+ for (const key of Object.keys(env)) {
69
+ if (key.startsWith(ENV_PREFIX))
70
+ names.push(key.slice(ENV_PREFIX.length));
71
+ }
72
+ return names;
73
+ },
74
+ };
75
+ }
76
+ /**
77
+ * Persist secrets as a JSON object. `dir` is a directory (`secrets.json`
78
+ * inside it) or a `.json` file path.
79
+ */
80
+ export function fileSecretDriver(dir) {
81
+ const file = secretsFile(dir);
82
+ return {
83
+ name: 'file',
84
+ get(name) {
85
+ return readJsonObject(file)[name];
86
+ },
87
+ set(name, value) {
88
+ const data = readJsonObject(file);
89
+ data[name] = value;
90
+ writeJsonObject(file, data);
91
+ },
92
+ delete(name) {
93
+ const data = readJsonObject(file);
94
+ if (!(name in data))
95
+ return false;
96
+ delete data[name];
97
+ writeJsonObject(file, data);
98
+ return true;
99
+ },
100
+ list() {
101
+ return Object.keys(readJsonObject(file));
102
+ },
103
+ };
104
+ }
105
+ function defaultKeyringSpawn(command, args, options = {}) {
106
+ try {
107
+ const result = spawnSync(command, [...args], {
108
+ encoding: 'utf8',
109
+ input: options.input,
110
+ timeout: 3000,
111
+ });
112
+ if (result.error)
113
+ return null;
114
+ return { status: result.status ?? 1, stdout: result.stdout ?? '' };
115
+ }
116
+ catch {
117
+ return null;
118
+ }
119
+ }
120
+ function toolAvailable(spawn, command) {
121
+ return spawn(command, ['--version']) !== null;
122
+ }
123
+ function fallbackKeyringFile(options) {
124
+ if (options.filePath !== undefined && options.filePath.length > 0)
125
+ return options.filePath;
126
+ const home = options.home ?? homedir();
127
+ return join(home, '.mudah', 'keyring.json');
128
+ }
129
+ function pickBackend(spawn, platform) {
130
+ const order = platform === 'darwin' ? ['security', 'secret-tool'] : ['secret-tool', 'security'];
131
+ for (const command of order) {
132
+ if (toolAvailable(spawn, command))
133
+ return command;
134
+ }
135
+ return 'file';
136
+ }
137
+ /**
138
+ * Best-effort OS keyring. Tries `secret-tool` (libsecret) or `security`
139
+ * (macOS) via an injectable spawn. When neither tool is available, secrets
140
+ * live in `~/.mudah/keyring.json`.
141
+ */
142
+ export function keyringSecretDriver(options = {}) {
143
+ const spawn = options.spawn ?? defaultKeyringSpawn;
144
+ const platform = options.platform ?? process.platform;
145
+ const fileDriver = fileSecretDriver(fallbackKeyringFile(options));
146
+ let backend;
147
+ const resolve = () => {
148
+ if (backend === undefined)
149
+ backend = pickBackend(spawn, platform);
150
+ return backend;
151
+ };
152
+ return {
153
+ name: 'keyring',
154
+ get(name) {
155
+ const kind = resolve();
156
+ if (kind === 'file')
157
+ return fileDriver.get(name);
158
+ if (kind === 'secret-tool') {
159
+ const result = spawn('secret-tool', ['lookup', 'service', 'mudah', 'key', name]);
160
+ if (result !== null && result.status === 0) {
161
+ const value = result.stdout.replace(/\n$/, '');
162
+ if (value.length > 0)
163
+ return value;
164
+ }
165
+ // A store that fell back to the file (tool missing/refused) must be
166
+ // readable here too, otherwise `set` writes something `get` can't see.
167
+ return fileDriver.get(name);
168
+ }
169
+ const result = spawn('security', ['find-generic-password', '-s', 'mudah', '-a', name, '-w']);
170
+ if (result !== null && result.status === 0) {
171
+ const value = result.stdout.replace(/\n$/, '');
172
+ if (value.length > 0)
173
+ return value;
174
+ }
175
+ // macOS writes go to the file backend (see `set`), so read it back.
176
+ return fileDriver.get(name);
177
+ },
178
+ set(name, value) {
179
+ const kind = resolve();
180
+ if (kind === 'file') {
181
+ fileDriver.set(name, value);
182
+ return;
183
+ }
184
+ if (kind === 'secret-tool') {
185
+ const result = spawn('secret-tool', ['store', '--label', `mudah:${name}`, 'service', 'mudah', 'key', name], { input: value });
186
+ if (result === null || result.status !== 0)
187
+ fileDriver.set(name, value);
188
+ return;
189
+ }
190
+ // macOS `security -w` puts the secret on argv (visible in `ps`).
191
+ // Writes go to the 0600 file backend instead.
192
+ fileDriver.set(name, value);
193
+ },
194
+ delete(name) {
195
+ const kind = resolve();
196
+ if (kind === 'file')
197
+ return fileDriver.delete(name);
198
+ if (kind === 'secret-tool') {
199
+ const result = spawn('secret-tool', ['clear', 'service', 'mudah', 'key', name]);
200
+ const fileRemoved = fileDriver.delete(name);
201
+ return (result !== null && result.status === 0) || fileRemoved;
202
+ }
203
+ const result = spawn('security', ['delete-generic-password', '-s', 'mudah', '-a', name]);
204
+ const fileRemoved = fileDriver.delete(name);
205
+ return (result !== null && result.status === 0) || fileRemoved;
206
+ },
207
+ list() {
208
+ const names = new Set(fileDriver.list());
209
+ const kind = resolve();
210
+ if (kind === 'secret-tool') {
211
+ // `secret-tool search` prints one block per match, with each stored
212
+ // attribute as `attribute.key = <name>`.
213
+ const result = spawn('secret-tool', ['search', '--all', 'service', 'mudah']);
214
+ if (result !== null && result.status === 0) {
215
+ for (const line of result.stdout.split('\n')) {
216
+ const match = /^\s*attribute\.key\s*=\s*(.+?)\s*$/.exec(line);
217
+ if (match?.[1] !== undefined)
218
+ names.add(match[1]);
219
+ }
220
+ }
221
+ }
222
+ return [...names].sort();
223
+ },
224
+ };
225
+ }
226
+ /**
227
+ * First-win secret store. `get` returns the first driver that has the name;
228
+ * `set` writes to the first driver; `delete` removes the name from every
229
+ * driver so a later one can't resurrect it.
230
+ */
231
+ export class SecretStore {
232
+ drivers;
233
+ constructor(options) {
234
+ this.drivers = options.drivers;
235
+ }
236
+ get(name) {
237
+ for (const driver of this.drivers) {
238
+ const value = driver.get(name);
239
+ if (value !== undefined)
240
+ return value;
241
+ }
242
+ return undefined;
243
+ }
244
+ set(name, value) {
245
+ const driver = this.drivers[0];
246
+ driver?.set(name, value);
247
+ }
248
+ delete(name) {
249
+ let removed = false;
250
+ for (const driver of this.drivers) {
251
+ if (driver.delete(name))
252
+ removed = true;
253
+ }
254
+ return removed;
255
+ }
256
+ list() {
257
+ const names = new Set();
258
+ for (const driver of this.drivers) {
259
+ for (const name of driver.list())
260
+ names.add(name);
261
+ }
262
+ return [...names];
263
+ }
264
+ /** Redacted view of every stored secret. Safe for `dump` / debug output. */
265
+ dump() {
266
+ const raw = {};
267
+ for (const name of this.list()) {
268
+ const value = this.get(name);
269
+ if (value !== undefined)
270
+ raw[name] = value;
271
+ }
272
+ return redactSecrets(raw, { keys: [/.+/] });
273
+ }
274
+ /** Alias of {@link dump} for debug printers. */
275
+ debug() {
276
+ return this.dump();
277
+ }
278
+ }
279
+ /** Build a {@link SecretStore} from an ordered driver list. */
280
+ export function createSecretStore(options) {
281
+ return new SecretStore(options);
282
+ }
283
+ const SECRET_REF = /^(env|file|keyring):(.+)$/;
284
+ /**
285
+ * Resolve a secret reference (`env:FOO`, `file:token`, `keyring:api`).
286
+ * Unknown or empty refs return `undefined`.
287
+ */
288
+ export function resolveSecret(ref, options = {}) {
289
+ const match = SECRET_REF.exec(ref);
290
+ if (!match)
291
+ return undefined;
292
+ const kind = match[1];
293
+ const name = match[2] ?? '';
294
+ if (name.length === 0)
295
+ return undefined;
296
+ if (kind === 'env')
297
+ return envSecretDriver(options.env).get(name);
298
+ if (kind === 'file')
299
+ return fileSecretDriver(options.fileDir ?? defaultSecretsDir()).get(name);
300
+ return keyringSecretDriver(options.keyring).get(name);
301
+ }
302
+ //# sourceMappingURL=secrets.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"secrets.js","sourceRoot":"","sources":["../src/secrets.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,36 @@
1
+ /**
2
+ * Minimal `fs.watch` surface. Tests inject a fake so they never touch the
3
+ * real filesystem or need a live file.
4
+ */
5
+ export type ConfigWatchFn = (path: string, listener: (eventType: string, filename: string | Buffer | null) => void) => {
6
+ close(): void;
7
+ };
8
+ export interface WatchConfigOptions {
9
+ /** Aborting this signal disposes the watchers. */
10
+ signal?: AbortSignal;
11
+ /** Watch implementation (default `fs.watch`). */
12
+ watch?: ConfigWatchFn;
13
+ }
14
+ /**
15
+ * Watch config files and invoke `onReload` when any of them change.
16
+ * Returns a disposer. Missing paths are skipped; `signal` abort is equivalent
17
+ * to calling the disposer.
18
+ */
19
+ export declare function watchConfig(paths: string | readonly string[], onReload: (path: string) => void, options?: WatchConfigOptions): () => void;
20
+ /**
21
+ * Enough of `process` to subscribe to SIGUSR1. Tests inject an EventEmitter
22
+ * so they never signal the real process.
23
+ */
24
+ export interface SignalProcess {
25
+ on(event: string, listener: (...args: unknown[]) => void): unknown;
26
+ removeListener?(event: string, listener: (...args: unknown[]) => void): unknown;
27
+ off?(event: string, listener: (...args: unknown[]) => void): unknown;
28
+ }
29
+ export interface ConfigReloadSignalOptions {
30
+ process?: SignalProcess;
31
+ }
32
+ /**
33
+ * Call `reload` when the process receives SIGUSR1. Returns a disposer that
34
+ * removes the handler.
35
+ */
36
+ export declare function installConfigReloadSignal(reload: () => void | Promise<void>, options?: ConfigReloadSignalOptions): () => void;
package/dist/watch.js ADDED
@@ -0,0 +1,57 @@
1
+ import { watch as fsWatch } from 'node:fs';
2
+ /**
3
+ * Watch config files and invoke `onReload` when any of them change.
4
+ * Returns a disposer. Missing paths are skipped; `signal` abort is equivalent
5
+ * to calling the disposer.
6
+ */
7
+ export function watchConfig(paths, onReload, options = {}) {
8
+ const list = typeof paths === 'string' ? [paths] : [...paths];
9
+ const watchFn = options.watch ?? defaultWatch;
10
+ const watchers = [];
11
+ const dispose = () => {
12
+ for (const watcher of watchers) {
13
+ try {
14
+ watcher.close();
15
+ }
16
+ catch {
17
+ // already closed
18
+ }
19
+ }
20
+ watchers.length = 0;
21
+ };
22
+ if (options.signal?.aborted)
23
+ return dispose;
24
+ for (const path of list) {
25
+ try {
26
+ watchers.push(watchFn(path, () => {
27
+ onReload(path);
28
+ }));
29
+ }
30
+ catch {
31
+ // Missing path, or the platform rejected the watch.
32
+ }
33
+ }
34
+ options.signal?.addEventListener('abort', dispose, { once: true });
35
+ return dispose;
36
+ }
37
+ function defaultWatch(path, listener) {
38
+ return fsWatch(path, listener);
39
+ }
40
+ /**
41
+ * Call `reload` when the process receives SIGUSR1. Returns a disposer that
42
+ * removes the handler.
43
+ */
44
+ export function installConfigReloadSignal(reload, options = {}) {
45
+ const proc = options.process ?? process;
46
+ const handler = () => {
47
+ void reload();
48
+ };
49
+ proc.on('SIGUSR1', handler);
50
+ return () => {
51
+ if (typeof proc.removeListener === 'function')
52
+ proc.removeListener('SIGUSR1', handler);
53
+ else
54
+ proc.off?.('SIGUSR1', handler);
55
+ };
56
+ }
57
+ //# sourceMappingURL=watch.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"watch.js","sourceRoot":"","sources":["../src/watch.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,KAAK,IAAI,OAAO,EAAE,MAAM,SAAS,CAAC;AAkB3C;;;;GAIG;AACH,MAAM,UAAU,WAAW,CACzB,KAAiC,EACjC,QAAgC,EAChC,OAAO,GAAuB,EAAE;IAEhC,MAAM,IAAI,GAAG,OAAO,KAAK,KAAK,QAAQ,CAAC,CAAC,CAAC,CAAC,KAAK,CAAC,CAAC,CAAC,CAAC,CAAC,GAAG,KAAK,CAAC,CAAC;IAC9D,MAAM,OAAO,GAAG,OAAO,CAAC,KAAK,IAAI,YAAY,CAAC;IAC9C,MAAM,QAAQ,GAA6B,EAAE,CAAC;IAE9C,MAAM,OAAO,GAAG,GAAS,EAAE;QACzB,KAAK,MAAM,OAAO,IAAI,QAAQ,EAAE,CAAC;YAC/B,IAAI,CAAC;gBACH,OAAO,CAAC,KAAK,EAAE,CAAC;YAClB,CAAC;YAAC,MAAM,CAAC;gBACP,iBAAiB;YACnB,CAAC;QACH,CAAC;QACD,QAAQ,CAAC,MAAM,GAAG,CAAC,CAAC;IACtB,CAAC,CAAC;IAEF,IAAI,OAAO,CAAC,MAAM,EAAE,OAAO;QAAE,OAAO,OAAO,CAAC;IAE5C,KAAK,MAAM,IAAI,IAAI,IAAI,EAAE,CAAC;QACxB,IAAI,CAAC;YACH,QAAQ,CAAC,IAAI,CACX,OAAO,CAAC,IAAI,EAAE,GAAG,EAAE;gBACjB,QAAQ,CAAC,IAAI,CAAC,CAAC;YACjB,CAAC,CAAC,CACH,CAAC;QACJ,CAAC;QAAC,MAAM,CAAC;YACP,oDAAoD;QACtD,CAAC;IACH,CAAC;IAED,OAAO,CAAC,MAAM,EAAE,gBAAgB,CAAC,OAAO,EAAE,OAAO,EAAE,EAAE,IAAI,EAAE,IAAI,EAAE,CAAC,CAAC;IACnE,OAAO,OAAO,CAAC;AACjB,CAAC;AAED,SAAS,YAAY,CACnB,IAAY,EACZ,QAAuE;IAEvE,OAAO,OAAO,CAAC,IAAI,EAAE,QAAQ,CAAC,CAAC;AACjC,CAAC;AAgBD;;;GAGG;AACH,MAAM,UAAU,yBAAyB,CACvC,MAAkC,EAClC,OAAO,GAA8B,EAAE;IAEvC,MAAM,IAAI,GAAG,OAAO,CAAC,OAAO,IAAI,OAAO,CAAC;IACxC,MAAM,OAAO,GAAG,GAAS,EAAE;QACzB,KAAK,MAAM,EAAE,CAAC;IAChB,CAAC,CAAC;IACF,IAAI,CAAC,EAAE,CAAC,SAAS,EAAE,OAAO,CAAC,CAAC;IAC5B,OAAO,GAAG,EAAE;QACV,IAAI,OAAO,IAAI,CAAC,cAAc,KAAK,UAAU;YAAE,IAAI,CAAC,cAAc,CAAC,SAAS,EAAE,OAAO,CAAC,CAAC;;YAClF,IAAI,CAAC,GAAG,EAAE,CAAC,SAAS,EAAE,OAAO,CAAC,CAAC;IACtC,CAAC,CAAC;AACJ,CAAC"}
package/package.json CHANGED
@@ -1,9 +1,23 @@
1
1
  {
2
2
  "name": "@mudah-cli/config",
3
- "version": "0.7.0",
3
+ "version": "0.9.0",
4
4
  "description": "Typed configuration repository, .env loading, and dotted-key access.",
5
5
  "type": "module",
6
6
  "license": "MIT",
7
+ "author": "Mudah Contributors",
8
+ "homepage": "https://github.com/thesimonharms/mudah#readme",
9
+ "repository": {
10
+ "type": "git",
11
+ "url": "git+https://github.com/thesimonharms/mudah.git",
12
+ "directory": "packages/config"
13
+ },
14
+ "keywords": [
15
+ "config",
16
+ "dotenv",
17
+ "schema",
18
+ "validation",
19
+ "typescript"
20
+ ],
7
21
  "engines": {
8
22
  "node": ">=26"
9
23
  },