@chriscode/hush 2.2.0 → 2.4.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/dist/cli.js +76 -7
- package/dist/commands/check.d.ts.map +1 -1
- package/dist/commands/check.js +67 -10
- package/dist/commands/init.d.ts.map +1 -1
- package/dist/commands/init.js +99 -4
- package/dist/commands/keys.d.ts +8 -0
- package/dist/commands/keys.d.ts.map +1 -0
- package/dist/commands/keys.js +136 -0
- package/dist/commands/set.d.ts.map +1 -1
- package/dist/commands/set.js +44 -11
- package/dist/config/loader.d.ts +5 -0
- package/dist/config/loader.d.ts.map +1 -1
- package/dist/config/loader.js +12 -2
- package/dist/lib/age.d.ts +16 -0
- package/dist/lib/age.d.ts.map +1 -0
- package/dist/lib/age.js +61 -0
- package/dist/lib/onepassword.d.ts +6 -0
- package/dist/lib/onepassword.d.ts.map +1 -0
- package/dist/lib/onepassword.js +48 -0
- package/dist/types.d.ts +11 -1
- package/dist/types.d.ts.map +1 -1
- package/dist/types.js +1 -0
- package/package.json +10 -9
- package/LICENSE +0 -21
package/dist/cli.js
CHANGED
|
@@ -13,7 +13,9 @@ import { inspectCommand } from './commands/inspect.js';
|
|
|
13
13
|
import { hasCommand } from './commands/has.js';
|
|
14
14
|
import { checkCommand } from './commands/check.js';
|
|
15
15
|
import { skillCommand } from './commands/skill.js';
|
|
16
|
-
|
|
16
|
+
import { keysCommand } from './commands/keys.js';
|
|
17
|
+
import { findConfigPath, loadConfig, checkSchemaVersion } from './config/loader.js';
|
|
18
|
+
const VERSION = '2.3.0';
|
|
17
19
|
function printHelp() {
|
|
18
20
|
console.log(`
|
|
19
21
|
${pc.bold('hush')} - SOPS-based secrets management for monorepos
|
|
@@ -34,6 +36,7 @@ ${pc.bold('Commands:')}
|
|
|
34
36
|
push Push secrets to Cloudflare Workers
|
|
35
37
|
status Show configuration and status
|
|
36
38
|
skill Install Claude Code / OpenCode skill
|
|
39
|
+
keys <cmd> Manage SOPS age keys (setup, generate, pull, push, list)
|
|
37
40
|
|
|
38
41
|
${pc.bold('Deprecated Commands:')}
|
|
39
42
|
decrypt Write secrets to disk (unsafe - use 'run' instead)
|
|
@@ -48,20 +51,24 @@ ${pc.bold('Options:')}
|
|
|
48
51
|
--json Output machine-readable JSON (check only)
|
|
49
52
|
--only-changed Only check git-modified files (check only)
|
|
50
53
|
--require-source Fail if source file is missing (check only)
|
|
54
|
+
--allow-plaintext Allow plaintext .env files (check only, not recommended)
|
|
51
55
|
--global Install skill to ~/.claude/skills/ (skill only)
|
|
52
56
|
--local Install skill to ./.claude/skills/ (skill/set only)
|
|
57
|
+
--gui Use macOS dialog for input (set only, for AI agents)
|
|
53
58
|
-h, --help Show this help message
|
|
54
59
|
-v, --version Show version number
|
|
55
60
|
|
|
56
61
|
${pc.bold('Examples:')}
|
|
57
|
-
hush init Initialize
|
|
62
|
+
hush init Initialize config + generate keys
|
|
58
63
|
hush encrypt Encrypt .env files
|
|
59
64
|
hush run -- npm start Run with secrets in memory (AI-safe!)
|
|
60
65
|
hush run -e prod -- npm build Run with production secrets
|
|
61
66
|
hush run -t api -- wrangler dev Run filtered for 'api' target
|
|
62
67
|
hush set DATABASE_URL Set a secret interactively (AI-safe)
|
|
68
|
+
hush set API_KEY --gui Set secret via macOS dialog (for AI agents)
|
|
63
69
|
hush set API_KEY -e prod Set a production secret
|
|
64
|
-
hush
|
|
70
|
+
hush keys setup Pull key from 1Password or verify local
|
|
71
|
+
hush keys generate Generate new key + backup to 1Password
|
|
65
72
|
hush edit Edit all shared secrets in $EDITOR
|
|
66
73
|
hush edit development Edit development secrets in $EDITOR
|
|
67
74
|
hush edit local Edit personal local overrides
|
|
@@ -92,6 +99,7 @@ function parseFileKey(value) {
|
|
|
92
99
|
}
|
|
93
100
|
function parseArgs(args) {
|
|
94
101
|
let command = '';
|
|
102
|
+
let subcommand;
|
|
95
103
|
let env = 'development';
|
|
96
104
|
let envExplicit = false;
|
|
97
105
|
let root = process.cwd();
|
|
@@ -101,8 +109,12 @@ function parseArgs(args) {
|
|
|
101
109
|
let json = false;
|
|
102
110
|
let onlyChanged = false;
|
|
103
111
|
let requireSource = false;
|
|
112
|
+
let allowPlaintext = false;
|
|
104
113
|
let global = false;
|
|
105
114
|
let local = false;
|
|
115
|
+
let force = false;
|
|
116
|
+
let gui = false;
|
|
117
|
+
let vault;
|
|
106
118
|
let file;
|
|
107
119
|
let key;
|
|
108
120
|
let target;
|
|
@@ -159,6 +171,10 @@ function parseArgs(args) {
|
|
|
159
171
|
requireSource = true;
|
|
160
172
|
continue;
|
|
161
173
|
}
|
|
174
|
+
if (arg === '--allow-plaintext') {
|
|
175
|
+
allowPlaintext = true;
|
|
176
|
+
continue;
|
|
177
|
+
}
|
|
162
178
|
if (arg === '--global') {
|
|
163
179
|
global = true;
|
|
164
180
|
continue;
|
|
@@ -167,6 +183,18 @@ function parseArgs(args) {
|
|
|
167
183
|
local = true;
|
|
168
184
|
continue;
|
|
169
185
|
}
|
|
186
|
+
if (arg === '--force' || arg === '-f') {
|
|
187
|
+
force = true;
|
|
188
|
+
continue;
|
|
189
|
+
}
|
|
190
|
+
if (arg === '--gui') {
|
|
191
|
+
gui = true;
|
|
192
|
+
continue;
|
|
193
|
+
}
|
|
194
|
+
if (arg === '--vault') {
|
|
195
|
+
vault = args[++i];
|
|
196
|
+
continue;
|
|
197
|
+
}
|
|
170
198
|
if (arg === '-t' || arg === '--target') {
|
|
171
199
|
target = args[++i];
|
|
172
200
|
continue;
|
|
@@ -199,8 +227,40 @@ function parseArgs(args) {
|
|
|
199
227
|
key = arg;
|
|
200
228
|
continue;
|
|
201
229
|
}
|
|
230
|
+
if (command === 'keys' && !arg.startsWith('-') && !subcommand) {
|
|
231
|
+
subcommand = arg;
|
|
232
|
+
continue;
|
|
233
|
+
}
|
|
234
|
+
}
|
|
235
|
+
return { command, subcommand, env, envExplicit, root, dryRun, quiet, warn, json, onlyChanged, requireSource, allowPlaintext, global, local, force, gui, vault, file, key, target, cmdArgs };
|
|
236
|
+
}
|
|
237
|
+
function checkMigrationNeeded(root, command) {
|
|
238
|
+
const skipCommands = ['', 'help', 'version', 'init', 'skill'];
|
|
239
|
+
if (skipCommands.includes(command))
|
|
240
|
+
return;
|
|
241
|
+
const configPath = findConfigPath(root);
|
|
242
|
+
if (!configPath)
|
|
243
|
+
return;
|
|
244
|
+
const config = loadConfig(root);
|
|
245
|
+
const { needsMigration, from, to } = checkSchemaVersion(config);
|
|
246
|
+
if (needsMigration) {
|
|
247
|
+
console.log('');
|
|
248
|
+
console.log(pc.yellow('━'.repeat(60)));
|
|
249
|
+
console.log(pc.yellow(pc.bold(' Migration Required')));
|
|
250
|
+
console.log(pc.yellow('━'.repeat(60)));
|
|
251
|
+
console.log('');
|
|
252
|
+
console.log(` Your ${pc.cyan('hush.yaml')} uses schema version ${pc.bold(String(from))}.`);
|
|
253
|
+
console.log(` Hush ${VERSION} uses schema version ${pc.bold(String(to))}.`);
|
|
254
|
+
console.log('');
|
|
255
|
+
console.log(pc.dim(' Migration guide:'));
|
|
256
|
+
console.log(` ${pc.cyan(`https://hush-docs.pages.dev/migrations/v${from}-to-v${to}`)}`);
|
|
257
|
+
console.log('');
|
|
258
|
+
console.log(pc.dim(' Or ask your AI assistant:'));
|
|
259
|
+
console.log(pc.dim(` "Help me migrate hush.yaml from schema v${from} to v${to}"`));
|
|
260
|
+
console.log('');
|
|
261
|
+
console.log(pc.yellow('━'.repeat(60)));
|
|
262
|
+
console.log('');
|
|
202
263
|
}
|
|
203
|
-
return { command, env, envExplicit, root, dryRun, quiet, warn, json, onlyChanged, requireSource, global, local, file, key, target, cmdArgs };
|
|
204
264
|
}
|
|
205
265
|
async function main() {
|
|
206
266
|
const args = process.argv.slice(2);
|
|
@@ -208,7 +268,8 @@ async function main() {
|
|
|
208
268
|
printHelp();
|
|
209
269
|
process.exit(0);
|
|
210
270
|
}
|
|
211
|
-
const { command, env, envExplicit, root, dryRun, quiet, warn, json, onlyChanged, requireSource, global, local, file, key, target, cmdArgs } = parseArgs(args);
|
|
271
|
+
const { command, subcommand, env, envExplicit, root, dryRun, quiet, warn, json, onlyChanged, requireSource, allowPlaintext, global, local, force, gui, vault, file, key, target, cmdArgs } = parseArgs(args);
|
|
272
|
+
checkMigrationNeeded(root, command);
|
|
212
273
|
try {
|
|
213
274
|
switch (command) {
|
|
214
275
|
case 'init':
|
|
@@ -241,7 +302,7 @@ async function main() {
|
|
|
241
302
|
else if (envExplicit) {
|
|
242
303
|
setFile = env;
|
|
243
304
|
}
|
|
244
|
-
await setCommand({ root, file: setFile, key });
|
|
305
|
+
await setCommand({ root, file: setFile, key, gui });
|
|
245
306
|
break;
|
|
246
307
|
}
|
|
247
308
|
case 'edit':
|
|
@@ -261,7 +322,7 @@ async function main() {
|
|
|
261
322
|
await hasCommand({ root, env, key, quiet });
|
|
262
323
|
break;
|
|
263
324
|
case 'check':
|
|
264
|
-
await checkCommand({ root, warn, json, quiet, onlyChanged, requireSource });
|
|
325
|
+
await checkCommand({ root, warn, json, quiet, onlyChanged, requireSource, allowPlaintext });
|
|
265
326
|
break;
|
|
266
327
|
case 'push':
|
|
267
328
|
await pushCommand({ root, dryRun });
|
|
@@ -272,6 +333,14 @@ async function main() {
|
|
|
272
333
|
case 'skill':
|
|
273
334
|
await skillCommand({ root, global, local });
|
|
274
335
|
break;
|
|
336
|
+
case 'keys':
|
|
337
|
+
if (!subcommand) {
|
|
338
|
+
console.error(pc.red('Usage: hush keys <command>'));
|
|
339
|
+
console.error(pc.dim('Commands: setup, generate, pull, push, list'));
|
|
340
|
+
process.exit(1);
|
|
341
|
+
}
|
|
342
|
+
await keysCommand({ root, subcommand, vault, force });
|
|
343
|
+
break;
|
|
275
344
|
default:
|
|
276
345
|
if (command) {
|
|
277
346
|
console.error(pc.red(`Unknown command: ${command}`));
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"check.d.ts","sourceRoot":"","sources":["../../src/commands/check.ts"],"names":[],"mappings":"AAQA,OAAO,KAAK,EAAE,YAAY,EAAmB,WAAW,
|
|
1
|
+
{"version":3,"file":"check.d.ts","sourceRoot":"","sources":["../../src/commands/check.ts"],"names":[],"mappings":"AAQA,OAAO,KAAK,EAAE,YAAY,EAAmB,WAAW,EAAmC,MAAM,aAAa,CAAC;AAkF/G,wBAAsB,KAAK,CAAC,OAAO,EAAE,YAAY,GAAG,OAAO,CAAC,WAAW,CAAC,CA+BvE;AAuMD,wBAAsB,YAAY,CAAC,OAAO,EAAE,YAAY,GAAG,OAAO,CAAC,IAAI,CAAC,CAiCvE"}
|
package/dist/commands/check.js
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
|
-
import { existsSync, readFileSync } from 'node:fs';
|
|
1
|
+
import { existsSync, readFileSync, readdirSync, statSync } from 'node:fs';
|
|
2
2
|
import { join } from 'node:path';
|
|
3
3
|
import { execSync } from 'node:child_process';
|
|
4
4
|
import pc from 'picocolors';
|
|
5
|
-
import { loadConfig
|
|
5
|
+
import { loadConfig } from '../config/loader.js';
|
|
6
6
|
import { parseEnvContent } from '../core/parse.js';
|
|
7
7
|
import { decrypt as sopsDecrypt, isSopsInstalled } from '../core/sops.js';
|
|
8
8
|
import { computeDiff, isInSync } from '../lib/diff.js';
|
|
@@ -42,8 +42,41 @@ function getGitChangedFiles(root) {
|
|
|
42
42
|
return new Set();
|
|
43
43
|
}
|
|
44
44
|
}
|
|
45
|
+
function findPlaintextEnvFiles(root) {
|
|
46
|
+
const results = [];
|
|
47
|
+
const plaintextPatterns = ['.env', '.env.development', '.env.production', '.env.local', '.env.staging', '.env.test', '.dev.vars'];
|
|
48
|
+
const skipDirs = new Set(['node_modules', '.git', 'dist', 'build', '.next', '.nuxt']);
|
|
49
|
+
function scanDir(dir, relativePath = '') {
|
|
50
|
+
let entries;
|
|
51
|
+
try {
|
|
52
|
+
entries = readdirSync(dir);
|
|
53
|
+
}
|
|
54
|
+
catch {
|
|
55
|
+
return;
|
|
56
|
+
}
|
|
57
|
+
for (const entry of entries) {
|
|
58
|
+
if (skipDirs.has(entry))
|
|
59
|
+
continue;
|
|
60
|
+
const fullPath = join(dir, entry);
|
|
61
|
+
const relPath = relativePath ? `${relativePath}/${entry}` : entry;
|
|
62
|
+
try {
|
|
63
|
+
if (statSync(fullPath).isDirectory()) {
|
|
64
|
+
scanDir(fullPath, relPath);
|
|
65
|
+
}
|
|
66
|
+
else if (plaintextPatterns.includes(entry)) {
|
|
67
|
+
results.push({ file: relPath, keyCount: 0 });
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
catch {
|
|
71
|
+
continue;
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
scanDir(root);
|
|
76
|
+
return results;
|
|
77
|
+
}
|
|
45
78
|
export async function check(options) {
|
|
46
|
-
const { root, requireSource, onlyChanged } = options;
|
|
79
|
+
const { root, requireSource, onlyChanged, allowPlaintext } = options;
|
|
47
80
|
if (!isSopsInstalled()) {
|
|
48
81
|
return {
|
|
49
82
|
status: 'error',
|
|
@@ -58,15 +91,17 @@ export async function check(options) {
|
|
|
58
91
|
}],
|
|
59
92
|
};
|
|
60
93
|
}
|
|
61
|
-
const configPath = findConfigPath(root);
|
|
62
|
-
if (!configPath) {
|
|
63
|
-
const config = loadConfig(root);
|
|
64
|
-
const pairs = getSourceEncryptedPairs(config);
|
|
65
|
-
return checkPairs(root, pairs, requireSource, onlyChanged);
|
|
66
|
-
}
|
|
67
94
|
const config = loadConfig(root);
|
|
68
95
|
const pairs = getSourceEncryptedPairs(config);
|
|
69
|
-
|
|
96
|
+
const result = checkPairs(root, pairs, requireSource, onlyChanged);
|
|
97
|
+
if (!allowPlaintext) {
|
|
98
|
+
const plaintextFiles = findPlaintextEnvFiles(root);
|
|
99
|
+
if (plaintextFiles.length > 0) {
|
|
100
|
+
result.plaintextFiles = plaintextFiles;
|
|
101
|
+
result.status = 'plaintext';
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
return result;
|
|
70
105
|
}
|
|
71
106
|
function checkPairs(root, pairs, requireSource, onlyChanged) {
|
|
72
107
|
const changedFiles = onlyChanged ? getGitChangedFiles(root) : null;
|
|
@@ -160,6 +195,25 @@ function checkPairs(root, pairs, requireSource, onlyChanged) {
|
|
|
160
195
|
function formatTextOutput(result) {
|
|
161
196
|
const lines = [];
|
|
162
197
|
lines.push('Checking secrets...\n');
|
|
198
|
+
if (result.plaintextFiles && result.plaintextFiles.length > 0) {
|
|
199
|
+
lines.push(pc.red(pc.bold('⚠ PLAINTEXT SECRETS DETECTED')));
|
|
200
|
+
lines.push('');
|
|
201
|
+
lines.push(pc.red('The following unencrypted .env files were found:'));
|
|
202
|
+
for (const pf of result.plaintextFiles) {
|
|
203
|
+
lines.push(pc.red(` • ${pf.file}`));
|
|
204
|
+
}
|
|
205
|
+
lines.push('');
|
|
206
|
+
lines.push(pc.yellow('These files contain plaintext secrets that could be exposed to AI assistants.'));
|
|
207
|
+
lines.push('');
|
|
208
|
+
lines.push(pc.bold('To fix:'));
|
|
209
|
+
lines.push(pc.dim(' 1. Run: hush encrypt'));
|
|
210
|
+
lines.push(pc.dim(' 2. Delete the plaintext files (the .encrypted versions are your source of truth)'));
|
|
211
|
+
lines.push(pc.dim(' 3. Add to .gitignore: .env, .env.*, .dev.vars'));
|
|
212
|
+
lines.push('');
|
|
213
|
+
lines.push(pc.dim('To allow plaintext files (not recommended): --allow-plaintext'));
|
|
214
|
+
lines.push('');
|
|
215
|
+
return lines.join('\n');
|
|
216
|
+
}
|
|
163
217
|
for (const file of result.files) {
|
|
164
218
|
if (file.error === 'SOPS_NOT_INSTALLED') {
|
|
165
219
|
lines.push(pc.red('Error: SOPS is not installed'));
|
|
@@ -232,6 +286,9 @@ export async function checkCommand(options) {
|
|
|
232
286
|
console.log(formatTextOutput(result));
|
|
233
287
|
}
|
|
234
288
|
}
|
|
289
|
+
if (result.status === 'plaintext' && !options.warn) {
|
|
290
|
+
process.exit(4);
|
|
291
|
+
}
|
|
235
292
|
if (result.status === 'error') {
|
|
236
293
|
const hasSopsError = result.files.some(f => f.error === 'SOPS_NOT_INSTALLED');
|
|
237
294
|
const hasDecryptError = result.files.some(f => f.error === 'DECRYPT_FAILED');
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"init.d.ts","sourceRoot":"","sources":["../../src/commands/init.ts"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"init.d.ts","sourceRoot":"","sources":["../../src/commands/init.ts"],"names":[],"mappings":"AAOA,OAAO,KAAK,EAAc,WAAW,EAAU,MAAM,aAAa,CAAC;AA2InE,wBAAsB,WAAW,CAAC,OAAO,EAAE,WAAW,GAAG,OAAO,CAAC,IAAI,CAAC,CAyCrE"}
|
package/dist/commands/init.js
CHANGED
|
@@ -1,9 +1,99 @@
|
|
|
1
|
-
import { existsSync, readdirSync, writeFileSync } from 'node:fs';
|
|
1
|
+
import { existsSync, readdirSync, readFileSync, writeFileSync } from 'node:fs';
|
|
2
2
|
import { join } from 'node:path';
|
|
3
3
|
import pc from 'picocolors';
|
|
4
4
|
import { stringify as stringifyYaml } from 'yaml';
|
|
5
5
|
import { findConfigPath } from '../config/loader.js';
|
|
6
|
+
import { ageAvailable, ageGenerate, keyExists, keySave, keyPath } from '../lib/age.js';
|
|
7
|
+
import { opAvailable, opGetKey, opStoreKey } from '../lib/onepassword.js';
|
|
6
8
|
import { DEFAULT_SOURCES } from '../types.js';
|
|
9
|
+
function getProjectFromPackageJson(root) {
|
|
10
|
+
const pkgPath = join(root, 'package.json');
|
|
11
|
+
if (!existsSync(pkgPath))
|
|
12
|
+
return null;
|
|
13
|
+
try {
|
|
14
|
+
const pkg = JSON.parse(readFileSync(pkgPath, 'utf-8'));
|
|
15
|
+
if (typeof pkg.repository === 'string') {
|
|
16
|
+
const match = pkg.repository.match(/github\.com[/:]([\w-]+\/[\w-]+)/);
|
|
17
|
+
if (match)
|
|
18
|
+
return match[1];
|
|
19
|
+
}
|
|
20
|
+
if (pkg.repository?.url) {
|
|
21
|
+
const match = pkg.repository.url.match(/github\.com[/:]([\w-]+\/[\w-]+)/);
|
|
22
|
+
if (match)
|
|
23
|
+
return match[1];
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
catch {
|
|
27
|
+
return null;
|
|
28
|
+
}
|
|
29
|
+
return null;
|
|
30
|
+
}
|
|
31
|
+
async function tryExistingLocalKey(project) {
|
|
32
|
+
if (!keyExists(project))
|
|
33
|
+
return null;
|
|
34
|
+
const existing = await import('../lib/age.js').then(m => m.keyLoad(project));
|
|
35
|
+
if (!existing)
|
|
36
|
+
return null;
|
|
37
|
+
console.log(pc.green(`Using existing key for ${pc.cyan(project)}`));
|
|
38
|
+
return { publicKey: existing.public, source: 'existing' };
|
|
39
|
+
}
|
|
40
|
+
async function tryPullFrom1Password(project) {
|
|
41
|
+
if (!opAvailable())
|
|
42
|
+
return null;
|
|
43
|
+
console.log(pc.dim('Checking 1Password for existing key...'));
|
|
44
|
+
const priv = opGetKey(project);
|
|
45
|
+
if (!priv)
|
|
46
|
+
return null;
|
|
47
|
+
const { agePublicFromPrivate } = await import('../lib/age.js');
|
|
48
|
+
const pub = agePublicFromPrivate(priv);
|
|
49
|
+
keySave(project, { private: priv, public: pub });
|
|
50
|
+
console.log(pc.green(`Pulled key from 1Password for ${pc.cyan(project)}`));
|
|
51
|
+
return { publicKey: pub, source: '1password' };
|
|
52
|
+
}
|
|
53
|
+
function generateAndBackupKey(project) {
|
|
54
|
+
if (!ageAvailable()) {
|
|
55
|
+
console.log(pc.yellow('age not installed. Run: brew install age'));
|
|
56
|
+
return null;
|
|
57
|
+
}
|
|
58
|
+
console.log(pc.blue(`Generating new key for ${pc.cyan(project)}...`));
|
|
59
|
+
const key = ageGenerate();
|
|
60
|
+
keySave(project, key);
|
|
61
|
+
console.log(pc.green(`Saved to ${keyPath(project)}`));
|
|
62
|
+
console.log(pc.dim(`Public: ${key.public}`));
|
|
63
|
+
if (opAvailable()) {
|
|
64
|
+
try {
|
|
65
|
+
opStoreKey(project, key.private, key.public);
|
|
66
|
+
console.log(pc.green('Backed up to 1Password.'));
|
|
67
|
+
}
|
|
68
|
+
catch (e) {
|
|
69
|
+
console.warn(pc.yellow(`Could not backup to 1Password: ${e.message}`));
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
return { publicKey: key.public, source: 'generated' };
|
|
73
|
+
}
|
|
74
|
+
async function setupKey(root, project) {
|
|
75
|
+
if (!project) {
|
|
76
|
+
console.log(pc.yellow('No project identifier found. Skipping key setup.'));
|
|
77
|
+
console.log(pc.dim('Add "project: my-project" to hush.yaml or set repository in package.json'));
|
|
78
|
+
return null;
|
|
79
|
+
}
|
|
80
|
+
return ((await tryExistingLocalKey(project)) ||
|
|
81
|
+
(await tryPullFrom1Password(project)) ||
|
|
82
|
+
generateAndBackupKey(project));
|
|
83
|
+
}
|
|
84
|
+
function createSopsConfig(root, publicKey) {
|
|
85
|
+
const sopsPath = join(root, '.sops.yaml');
|
|
86
|
+
if (existsSync(sopsPath)) {
|
|
87
|
+
console.log(pc.yellow('.sops.yaml already exists. Add this public key if needed:'));
|
|
88
|
+
console.log(` ${publicKey}`);
|
|
89
|
+
return;
|
|
90
|
+
}
|
|
91
|
+
const sopsConfig = stringifyYaml({
|
|
92
|
+
creation_rules: [{ encrypted_regex: '.*', age: publicKey }]
|
|
93
|
+
});
|
|
94
|
+
writeFileSync(sopsPath, sopsConfig, 'utf-8');
|
|
95
|
+
console.log(pc.green('Created .sops.yaml'));
|
|
96
|
+
}
|
|
7
97
|
function detectTargets(root) {
|
|
8
98
|
const targets = [{ name: 'root', path: '.', format: 'dotenv' }];
|
|
9
99
|
const entries = readdirSync(root, { withFileTypes: true });
|
|
@@ -43,10 +133,16 @@ export async function initCommand(options) {
|
|
|
43
133
|
process.exit(1);
|
|
44
134
|
}
|
|
45
135
|
console.log(pc.blue('Initializing hush...'));
|
|
136
|
+
const project = getProjectFromPackageJson(root);
|
|
137
|
+
const keyResult = await setupKey(root, project);
|
|
138
|
+
if (keyResult) {
|
|
139
|
+
createSopsConfig(root, keyResult.publicKey);
|
|
140
|
+
}
|
|
46
141
|
const targets = detectTargets(root);
|
|
47
142
|
const config = {
|
|
48
143
|
sources: DEFAULT_SOURCES,
|
|
49
144
|
targets,
|
|
145
|
+
...(project && { project }),
|
|
50
146
|
};
|
|
51
147
|
const yaml = stringifyYaml(config, { indent: 2 });
|
|
52
148
|
const configPath = join(root, 'hush.yaml');
|
|
@@ -57,7 +153,6 @@ export async function initCommand(options) {
|
|
|
57
153
|
console.log(` ${pc.cyan(target.name)} ${pc.dim(target.path)} ${pc.magenta(target.format)}`);
|
|
58
154
|
}
|
|
59
155
|
console.log(pc.dim('\nNext steps:'));
|
|
60
|
-
console.log(' 1.
|
|
61
|
-
console.log(' 2. Run "hush
|
|
62
|
-
console.log(' 3. Run "hush decrypt" to generate local env files');
|
|
156
|
+
console.log(' 1. Run "hush set <KEY>" to add secrets');
|
|
157
|
+
console.log(' 2. Run "hush run -- <command>" to run with secrets in memory');
|
|
63
158
|
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"keys.d.ts","sourceRoot":"","sources":["../../src/commands/keys.ts"],"names":[],"mappings":"AAQA,MAAM,WAAW,WAAW;IAC1B,IAAI,EAAE,MAAM,CAAC;IACb,UAAU,EAAE,MAAM,CAAC;IACnB,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,KAAK,CAAC,EAAE,OAAO,CAAC;CACjB;AAwBD,wBAAsB,WAAW,CAAC,OAAO,EAAE,WAAW,GAAG,OAAO,CAAC,IAAI,CAAC,CA6HrE"}
|
|
@@ -0,0 +1,136 @@
|
|
|
1
|
+
import { existsSync, readFileSync, writeFileSync } from 'node:fs';
|
|
2
|
+
import { join } from 'node:path';
|
|
3
|
+
import pc from 'picocolors';
|
|
4
|
+
import { stringify as yamlStringify } from 'yaml';
|
|
5
|
+
import { loadConfig } from '../config/loader.js';
|
|
6
|
+
import { opAvailable, opGetKey, opStoreKey, opListKeys } from '../lib/onepassword.js';
|
|
7
|
+
import { ageAvailable, ageGenerate, agePublicFromPrivate, keyExists, keySave, keyLoad, keysList, keyPath } from '../lib/age.js';
|
|
8
|
+
function getProject(root) {
|
|
9
|
+
const config = loadConfig(root);
|
|
10
|
+
if (config.project)
|
|
11
|
+
return config.project;
|
|
12
|
+
const pkgPath = join(root, 'package.json');
|
|
13
|
+
if (existsSync(pkgPath)) {
|
|
14
|
+
const pkg = JSON.parse(readFileSync(pkgPath, 'utf-8'));
|
|
15
|
+
if (typeof pkg.repository === 'string') {
|
|
16
|
+
const match = pkg.repository.match(/github\.com[/:]([\w-]+\/[\w-]+)/);
|
|
17
|
+
if (match)
|
|
18
|
+
return match[1];
|
|
19
|
+
}
|
|
20
|
+
if (pkg.repository?.url) {
|
|
21
|
+
const match = pkg.repository.url.match(/github\.com[/:]([\w-]+\/[\w-]+)/);
|
|
22
|
+
if (match)
|
|
23
|
+
return match[1];
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
console.error(pc.red('No project identifier found.'));
|
|
27
|
+
console.error(pc.dim('Add "project: my-project" to hush.yaml'));
|
|
28
|
+
process.exit(1);
|
|
29
|
+
}
|
|
30
|
+
export async function keysCommand(options) {
|
|
31
|
+
const { root, subcommand, vault, force } = options;
|
|
32
|
+
switch (subcommand) {
|
|
33
|
+
case 'setup': {
|
|
34
|
+
const project = getProject(root);
|
|
35
|
+
console.log(pc.blue(`Setting up keys for ${pc.cyan(project)}...`));
|
|
36
|
+
if (keyExists(project)) {
|
|
37
|
+
console.log(pc.green('Key already exists locally.'));
|
|
38
|
+
return;
|
|
39
|
+
}
|
|
40
|
+
if (opAvailable()) {
|
|
41
|
+
const priv = opGetKey(project, vault);
|
|
42
|
+
if (priv) {
|
|
43
|
+
const pub = agePublicFromPrivate(priv);
|
|
44
|
+
keySave(project, { private: priv, public: pub });
|
|
45
|
+
console.log(pc.green('Pulled key from 1Password.'));
|
|
46
|
+
return;
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
console.log(pc.yellow('No key found. Run "hush keys generate" to create one.'));
|
|
50
|
+
break;
|
|
51
|
+
}
|
|
52
|
+
case 'generate': {
|
|
53
|
+
if (!ageAvailable()) {
|
|
54
|
+
console.error(pc.red('age not installed. Run: brew install age'));
|
|
55
|
+
process.exit(1);
|
|
56
|
+
}
|
|
57
|
+
const project = getProject(root);
|
|
58
|
+
if (keyExists(project) && !force) {
|
|
59
|
+
console.error(pc.yellow(`Key exists for ${project}. Use --force to overwrite.`));
|
|
60
|
+
process.exit(1);
|
|
61
|
+
}
|
|
62
|
+
console.log(pc.blue(`Generating key for ${pc.cyan(project)}...`));
|
|
63
|
+
const key = ageGenerate();
|
|
64
|
+
keySave(project, key);
|
|
65
|
+
console.log(pc.green(`Saved to ${keyPath(project)}`));
|
|
66
|
+
console.log(pc.dim(`Public: ${key.public}`));
|
|
67
|
+
if (opAvailable()) {
|
|
68
|
+
try {
|
|
69
|
+
opStoreKey(project, key.private, key.public, vault);
|
|
70
|
+
console.log(pc.green('Stored in 1Password.'));
|
|
71
|
+
}
|
|
72
|
+
catch (e) {
|
|
73
|
+
console.warn(pc.yellow(`Could not store in 1Password: ${e.message}`));
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
const sopsPath = join(root, '.sops.yaml');
|
|
77
|
+
if (!existsSync(sopsPath)) {
|
|
78
|
+
writeFileSync(sopsPath, yamlStringify({ creation_rules: [{ encrypted_regex: '.*', age: key.public }] }));
|
|
79
|
+
console.log(pc.green('Created .sops.yaml'));
|
|
80
|
+
}
|
|
81
|
+
else {
|
|
82
|
+
console.log(pc.yellow('.sops.yaml exists. Add this public key:'));
|
|
83
|
+
console.log(` ${key.public}`);
|
|
84
|
+
}
|
|
85
|
+
break;
|
|
86
|
+
}
|
|
87
|
+
case 'pull': {
|
|
88
|
+
if (!opAvailable()) {
|
|
89
|
+
console.error(pc.red('1Password CLI not available or not signed in.'));
|
|
90
|
+
process.exit(1);
|
|
91
|
+
}
|
|
92
|
+
const project = getProject(root);
|
|
93
|
+
const priv = opGetKey(project, vault);
|
|
94
|
+
if (!priv) {
|
|
95
|
+
console.error(pc.red(`No key in 1Password for ${project}`));
|
|
96
|
+
process.exit(1);
|
|
97
|
+
}
|
|
98
|
+
const pub = agePublicFromPrivate(priv);
|
|
99
|
+
keySave(project, { private: priv, public: pub });
|
|
100
|
+
console.log(pc.green(`Pulled and saved to ${keyPath(project)}`));
|
|
101
|
+
break;
|
|
102
|
+
}
|
|
103
|
+
case 'push': {
|
|
104
|
+
if (!opAvailable()) {
|
|
105
|
+
console.error(pc.red('1Password CLI not available or not signed in.'));
|
|
106
|
+
process.exit(1);
|
|
107
|
+
}
|
|
108
|
+
const project = getProject(root);
|
|
109
|
+
const key = keyLoad(project);
|
|
110
|
+
if (!key) {
|
|
111
|
+
console.error(pc.red(`No local key for ${project}`));
|
|
112
|
+
process.exit(1);
|
|
113
|
+
}
|
|
114
|
+
opStoreKey(project, key.private, key.public, vault);
|
|
115
|
+
console.log(pc.green('Pushed to 1Password.'));
|
|
116
|
+
break;
|
|
117
|
+
}
|
|
118
|
+
case 'list': {
|
|
119
|
+
console.log(pc.blue('Local keys:'));
|
|
120
|
+
for (const k of keysList()) {
|
|
121
|
+
console.log(` ${pc.cyan(k.project)} ${pc.dim(k.public.slice(0, 20))}...`);
|
|
122
|
+
}
|
|
123
|
+
if (opAvailable()) {
|
|
124
|
+
console.log(pc.blue('\n1Password keys:'));
|
|
125
|
+
for (const project of opListKeys(vault)) {
|
|
126
|
+
console.log(` ${pc.cyan(project)}`);
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
break;
|
|
130
|
+
}
|
|
131
|
+
default:
|
|
132
|
+
console.error(pc.red(`Unknown: hush keys ${subcommand}`));
|
|
133
|
+
console.log(pc.dim('Commands: setup, generate, pull, push, list'));
|
|
134
|
+
process.exit(1);
|
|
135
|
+
}
|
|
136
|
+
}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"set.d.ts","sourceRoot":"","sources":["../../src/commands/set.ts"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"set.d.ts","sourceRoot":"","sources":["../../src/commands/set.ts"],"names":[],"mappings":"AAOA,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,aAAa,CAAC;AAyF9C,wBAAsB,UAAU,CAAC,OAAO,EAAE,UAAU,GAAG,OAAO,CAAC,IAAI,CAAC,CA0CnE"}
|
package/dist/commands/set.js
CHANGED
|
@@ -1,14 +1,25 @@
|
|
|
1
|
+
import { execSync } from 'node:child_process';
|
|
1
2
|
import { existsSync } from 'node:fs';
|
|
2
3
|
import { join } from 'node:path';
|
|
4
|
+
import { platform } from 'node:os';
|
|
3
5
|
import pc from 'picocolors';
|
|
4
6
|
import { loadConfig } from '../config/loader.js';
|
|
5
7
|
import { setKey } from '../core/sops.js';
|
|
6
|
-
function
|
|
8
|
+
function promptViaMacOSDialog(key) {
|
|
9
|
+
try {
|
|
10
|
+
const script = `display dialog "Enter value for ${key}:" default answer "" with hidden answer with title "Hush - Set Secret"`;
|
|
11
|
+
const result = execSync(`osascript -e '${script}' -e 'text returned of result'`, {
|
|
12
|
+
encoding: 'utf-8',
|
|
13
|
+
stdio: ['pipe', 'pipe', 'pipe'],
|
|
14
|
+
});
|
|
15
|
+
return result.trim();
|
|
16
|
+
}
|
|
17
|
+
catch {
|
|
18
|
+
return null;
|
|
19
|
+
}
|
|
20
|
+
}
|
|
21
|
+
function promptViaTTY(key) {
|
|
7
22
|
return new Promise((resolve, reject) => {
|
|
8
|
-
if (!process.stdin.isTTY) {
|
|
9
|
-
reject(new Error('Interactive input requires a terminal (TTY)'));
|
|
10
|
-
return;
|
|
11
|
-
}
|
|
12
23
|
process.stdout.write(`Enter value for ${pc.cyan(key)}: `);
|
|
13
24
|
const stdin = process.stdin;
|
|
14
25
|
stdin.setRawMode(true);
|
|
@@ -19,21 +30,21 @@ function promptForValue(key) {
|
|
|
19
30
|
switch (char) {
|
|
20
31
|
case '\n':
|
|
21
32
|
case '\r':
|
|
22
|
-
case '\u0004':
|
|
33
|
+
case '\u0004':
|
|
23
34
|
stdin.setRawMode(false);
|
|
24
35
|
stdin.pause();
|
|
25
36
|
stdin.removeListener('data', onData);
|
|
26
37
|
process.stdout.write('\n');
|
|
27
38
|
resolve(value);
|
|
28
39
|
break;
|
|
29
|
-
case '\u0003':
|
|
40
|
+
case '\u0003':
|
|
30
41
|
stdin.setRawMode(false);
|
|
31
42
|
stdin.pause();
|
|
32
43
|
stdin.removeListener('data', onData);
|
|
33
44
|
process.stdout.write('\n');
|
|
34
45
|
reject(new Error('Cancelled'));
|
|
35
46
|
break;
|
|
36
|
-
case '\u007F':
|
|
47
|
+
case '\u007F':
|
|
37
48
|
case '\b':
|
|
38
49
|
if (value.length > 0) {
|
|
39
50
|
value = value.slice(0, -1);
|
|
@@ -42,14 +53,36 @@ function promptForValue(key) {
|
|
|
42
53
|
break;
|
|
43
54
|
default:
|
|
44
55
|
value += char;
|
|
45
|
-
process.stdout.write('\u2022');
|
|
56
|
+
process.stdout.write('\u2022');
|
|
46
57
|
}
|
|
47
58
|
};
|
|
48
59
|
stdin.on('data', onData);
|
|
49
60
|
});
|
|
50
61
|
}
|
|
62
|
+
async function promptForValue(key, forceGui) {
|
|
63
|
+
if (forceGui && platform() === 'darwin') {
|
|
64
|
+
console.log(pc.dim('Opening dialog for secret input...'));
|
|
65
|
+
const value = promptViaMacOSDialog(key);
|
|
66
|
+
if (value !== null) {
|
|
67
|
+
return value;
|
|
68
|
+
}
|
|
69
|
+
throw new Error('Dialog cancelled or failed');
|
|
70
|
+
}
|
|
71
|
+
if (process.stdin.isTTY) {
|
|
72
|
+
return promptViaTTY(key);
|
|
73
|
+
}
|
|
74
|
+
if (platform() === 'darwin') {
|
|
75
|
+
console.log(pc.dim('Opening dialog for secret input...'));
|
|
76
|
+
const value = promptViaMacOSDialog(key);
|
|
77
|
+
if (value !== null) {
|
|
78
|
+
return value;
|
|
79
|
+
}
|
|
80
|
+
throw new Error('Dialog cancelled or failed');
|
|
81
|
+
}
|
|
82
|
+
throw new Error('Interactive input requires a terminal (TTY) or macOS');
|
|
83
|
+
}
|
|
51
84
|
export async function setCommand(options) {
|
|
52
|
-
const { root, file, key } = options;
|
|
85
|
+
const { root, file, key, gui } = options;
|
|
53
86
|
const config = loadConfig(root);
|
|
54
87
|
const fileKey = file ?? 'shared';
|
|
55
88
|
const sourcePath = config.sources[fileKey];
|
|
@@ -67,7 +100,7 @@ export async function setCommand(options) {
|
|
|
67
100
|
process.exit(1);
|
|
68
101
|
}
|
|
69
102
|
try {
|
|
70
|
-
const value = await promptForValue(key);
|
|
103
|
+
const value = await promptForValue(key, gui ?? false);
|
|
71
104
|
if (!value) {
|
|
72
105
|
console.error(pc.yellow('No value entered, aborting'));
|
|
73
106
|
process.exit(1);
|
package/dist/config/loader.d.ts
CHANGED
|
@@ -1,5 +1,10 @@
|
|
|
1
1
|
import type { HushConfig } from '../types.js';
|
|
2
2
|
export declare function findConfigPath(root: string): string | null;
|
|
3
3
|
export declare function loadConfig(root: string): HushConfig;
|
|
4
|
+
export declare function checkSchemaVersion(config: HushConfig): {
|
|
5
|
+
needsMigration: boolean;
|
|
6
|
+
from: number;
|
|
7
|
+
to: number;
|
|
8
|
+
};
|
|
4
9
|
export declare function validateConfig(config: HushConfig): string[];
|
|
5
10
|
//# sourceMappingURL=loader.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"loader.d.ts","sourceRoot":"","sources":["../../src/config/loader.ts"],"names":[],"mappings":"AAGA,OAAO,KAAK,EAAE,UAAU,
|
|
1
|
+
{"version":3,"file":"loader.d.ts","sourceRoot":"","sources":["../../src/config/loader.ts"],"names":[],"mappings":"AAGA,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,aAAa,CAAC;AAK9C,wBAAgB,cAAc,CAAC,IAAI,EAAE,MAAM,GAAG,MAAM,GAAG,IAAI,CAQ1D;AAED,wBAAgB,UAAU,CAAC,IAAI,EAAE,MAAM,GAAG,UAAU,CAmBnD;AAED,wBAAgB,kBAAkB,CAAC,MAAM,EAAE,UAAU,GAAG;IAAE,cAAc,EAAE,OAAO,CAAC;IAAC,IAAI,EAAE,MAAM,CAAC;IAAC,EAAE,EAAE,MAAM,CAAA;CAAE,CAO5G;AAED,wBAAgB,cAAc,CAAC,MAAM,EAAE,UAAU,GAAG,MAAM,EAAE,CAuB3D"}
|
package/dist/config/loader.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { existsSync, readFileSync } from 'node:fs';
|
|
2
2
|
import { join } from 'node:path';
|
|
3
3
|
import { parse as parseYaml } from 'yaml';
|
|
4
|
-
import { DEFAULT_SOURCES } from '../types.js';
|
|
4
|
+
import { DEFAULT_SOURCES, CURRENT_SCHEMA_VERSION } from '../types.js';
|
|
5
5
|
const CONFIG_FILENAMES = ['hush.yaml', 'hush.yml'];
|
|
6
6
|
export function findConfigPath(root) {
|
|
7
7
|
for (const filename of CONFIG_FILENAMES) {
|
|
@@ -23,10 +23,20 @@ export function loadConfig(root) {
|
|
|
23
23
|
const content = readFileSync(configPath, 'utf-8');
|
|
24
24
|
const parsed = parseYaml(content);
|
|
25
25
|
return {
|
|
26
|
+
schema_version: parsed.schema_version,
|
|
27
|
+
project: parsed.project,
|
|
26
28
|
sources: { ...DEFAULT_SOURCES, ...parsed.sources },
|
|
27
29
|
targets: parsed.targets ?? [{ name: 'root', path: '.', format: 'dotenv' }],
|
|
28
30
|
};
|
|
29
31
|
}
|
|
32
|
+
export function checkSchemaVersion(config) {
|
|
33
|
+
const configVersion = config.schema_version ?? 1;
|
|
34
|
+
return {
|
|
35
|
+
needsMigration: configVersion < CURRENT_SCHEMA_VERSION,
|
|
36
|
+
from: configVersion,
|
|
37
|
+
to: CURRENT_SCHEMA_VERSION,
|
|
38
|
+
};
|
|
39
|
+
}
|
|
30
40
|
export function validateConfig(config) {
|
|
31
41
|
const errors = [];
|
|
32
42
|
if (!config.sources.shared) {
|
|
@@ -42,7 +52,7 @@ export function validateConfig(config) {
|
|
|
42
52
|
if (!target.format) {
|
|
43
53
|
errors.push(`Target "${target.name}" must have a format`);
|
|
44
54
|
}
|
|
45
|
-
if (!['dotenv', 'wrangler', 'json', 'shell'].includes(target.format)) {
|
|
55
|
+
if (!['dotenv', 'wrangler', 'json', 'shell', 'yaml'].includes(target.format)) {
|
|
46
56
|
errors.push(`Target "${target.name}" has invalid format "${target.format}"`);
|
|
47
57
|
}
|
|
48
58
|
}
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
export interface AgeKey {
|
|
2
|
+
private: string;
|
|
3
|
+
public: string;
|
|
4
|
+
}
|
|
5
|
+
export declare function ageAvailable(): boolean;
|
|
6
|
+
export declare function ageGenerate(): AgeKey;
|
|
7
|
+
export declare function agePublicFromPrivate(privateKey: string): string;
|
|
8
|
+
export declare function keyPath(project: string): string;
|
|
9
|
+
export declare function keyExists(project: string): boolean;
|
|
10
|
+
export declare function keySave(project: string, key: AgeKey): void;
|
|
11
|
+
export declare function keyLoad(project: string): AgeKey | null;
|
|
12
|
+
export declare function keysList(): {
|
|
13
|
+
project: string;
|
|
14
|
+
public: string;
|
|
15
|
+
}[];
|
|
16
|
+
//# sourceMappingURL=age.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"age.d.ts","sourceRoot":"","sources":["../../src/lib/age.ts"],"names":[],"mappings":"AAKA,MAAM,WAAW,MAAM;IACrB,OAAO,EAAE,MAAM,CAAC;IAChB,MAAM,EAAE,MAAM,CAAC;CAChB;AAID,wBAAgB,YAAY,IAAI,OAAO,CAOtC;AAED,wBAAgB,WAAW,IAAI,MAAM,CAMpC;AAED,wBAAgB,oBAAoB,CAAC,UAAU,EAAE,MAAM,GAAG,MAAM,CAK/D;AAED,wBAAgB,OAAO,CAAC,OAAO,EAAE,MAAM,GAAG,MAAM,CAE/C;AAED,wBAAgB,SAAS,CAAC,OAAO,EAAE,MAAM,GAAG,OAAO,CAElD;AAED,wBAAgB,OAAO,CAAC,OAAO,EAAE,MAAM,EAAE,GAAG,EAAE,MAAM,GAAG,IAAI,CAI1D;AAED,wBAAgB,OAAO,CAAC,OAAO,EAAE,MAAM,GAAG,MAAM,GAAG,IAAI,CAStD;AAED,wBAAgB,QAAQ,IAAI;IAAE,OAAO,EAAE,MAAM,CAAC;IAAC,MAAM,EAAE,MAAM,CAAA;CAAE,EAAE,CAYhE"}
|
package/dist/lib/age.js
ADDED
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
import { execSync } from 'node:child_process';
|
|
2
|
+
import { existsSync, mkdirSync, readFileSync, readdirSync, writeFileSync } from 'node:fs';
|
|
3
|
+
import { join, dirname } from 'node:path';
|
|
4
|
+
import { homedir } from 'node:os';
|
|
5
|
+
const KEYS_DIR = join(homedir(), '.config', 'sops', 'age', 'keys');
|
|
6
|
+
export function ageAvailable() {
|
|
7
|
+
try {
|
|
8
|
+
execSync('which age-keygen', { stdio: 'pipe' });
|
|
9
|
+
return true;
|
|
10
|
+
}
|
|
11
|
+
catch {
|
|
12
|
+
return false;
|
|
13
|
+
}
|
|
14
|
+
}
|
|
15
|
+
export function ageGenerate() {
|
|
16
|
+
const output = execSync('age-keygen', { encoding: 'utf-8' });
|
|
17
|
+
const pub = output.match(/public key: (age1[a-z0-9]+)/)?.[1];
|
|
18
|
+
const priv = output.match(/(AGE-SECRET-KEY-[A-Z0-9]+)/)?.[1];
|
|
19
|
+
if (!pub || !priv)
|
|
20
|
+
throw new Error('Failed to generate age key');
|
|
21
|
+
return { private: priv, public: pub };
|
|
22
|
+
}
|
|
23
|
+
export function agePublicFromPrivate(privateKey) {
|
|
24
|
+
return execSync(`echo "${privateKey}" | age-keygen -y`, {
|
|
25
|
+
encoding: 'utf-8',
|
|
26
|
+
shell: '/bin/bash',
|
|
27
|
+
}).trim();
|
|
28
|
+
}
|
|
29
|
+
export function keyPath(project) {
|
|
30
|
+
return join(KEYS_DIR, `${project.replace(/\//g, '-')}.txt`);
|
|
31
|
+
}
|
|
32
|
+
export function keyExists(project) {
|
|
33
|
+
return existsSync(keyPath(project));
|
|
34
|
+
}
|
|
35
|
+
export function keySave(project, key) {
|
|
36
|
+
const path = keyPath(project);
|
|
37
|
+
mkdirSync(dirname(path), { recursive: true });
|
|
38
|
+
writeFileSync(path, `# project: ${project}\n# public key: ${key.public}\n${key.private}\n`, { mode: 0o600 });
|
|
39
|
+
}
|
|
40
|
+
export function keyLoad(project) {
|
|
41
|
+
const path = keyPath(project);
|
|
42
|
+
if (!existsSync(path))
|
|
43
|
+
return null;
|
|
44
|
+
const content = readFileSync(path, 'utf-8');
|
|
45
|
+
const pub = content.match(/# public key: (age1[a-z0-9]+)/)?.[1];
|
|
46
|
+
const priv = content.match(/(AGE-SECRET-KEY-[A-Z0-9]+)/)?.[1];
|
|
47
|
+
return pub && priv ? { private: priv, public: pub } : null;
|
|
48
|
+
}
|
|
49
|
+
export function keysList() {
|
|
50
|
+
if (!existsSync(KEYS_DIR))
|
|
51
|
+
return [];
|
|
52
|
+
return readdirSync(KEYS_DIR)
|
|
53
|
+
.filter(f => f.endsWith('.txt'))
|
|
54
|
+
.map(f => {
|
|
55
|
+
const content = readFileSync(join(KEYS_DIR, f), 'utf-8');
|
|
56
|
+
const project = content.match(/# project: (.+)/)?.[1] ?? content.match(/# repo: (.+)/)?.[1];
|
|
57
|
+
const pub = content.match(/# public key: (age1[a-z0-9]+)/)?.[1];
|
|
58
|
+
return project && pub ? { project, public: pub } : null;
|
|
59
|
+
})
|
|
60
|
+
.filter((k) => k !== null);
|
|
61
|
+
}
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
export declare const OP_ITEM_PREFIX = "SOPS Key - ";
|
|
2
|
+
export declare function opAvailable(): boolean;
|
|
3
|
+
export declare function opGetKey(project: string, vault?: string): string | null;
|
|
4
|
+
export declare function opStoreKey(project: string, privateKey: string, publicKey: string, vault?: string): void;
|
|
5
|
+
export declare function opListKeys(vault?: string): string[];
|
|
6
|
+
//# sourceMappingURL=onepassword.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"onepassword.d.ts","sourceRoot":"","sources":["../../src/lib/onepassword.ts"],"names":[],"mappings":"AAEA,eAAO,MAAM,cAAc,gBAAgB,CAAC;AAE5C,wBAAgB,WAAW,IAAI,OAAO,CAOrC;AAED,wBAAgB,QAAQ,CAAC,OAAO,EAAE,MAAM,EAAE,KAAK,CAAC,EAAE,MAAM,GAAG,MAAM,GAAG,IAAI,CAWvE;AAED,wBAAgB,UAAU,CAAC,OAAO,EAAE,MAAM,EAAE,UAAU,EAAE,MAAM,EAAE,SAAS,EAAE,MAAM,EAAE,KAAK,CAAC,EAAE,MAAM,GAAG,IAAI,CAcvG;AAED,wBAAgB,UAAU,CAAC,KAAK,CAAC,EAAE,MAAM,GAAG,MAAM,EAAE,CAcnD"}
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
import { execSync, spawnSync } from 'node:child_process';
|
|
2
|
+
export const OP_ITEM_PREFIX = 'SOPS Key - ';
|
|
3
|
+
export function opAvailable() {
|
|
4
|
+
try {
|
|
5
|
+
execSync('op whoami', { stdio: 'pipe' });
|
|
6
|
+
return true;
|
|
7
|
+
}
|
|
8
|
+
catch {
|
|
9
|
+
return false;
|
|
10
|
+
}
|
|
11
|
+
}
|
|
12
|
+
export function opGetKey(project, vault) {
|
|
13
|
+
try {
|
|
14
|
+
const vaultArgs = vault ? ['--vault', vault] : [];
|
|
15
|
+
const result = execSync(['op', 'item', 'get', `${OP_ITEM_PREFIX}${project}`, ...vaultArgs, '--fields', 'password', '--reveal'].join(' '), { encoding: 'utf-8', stdio: 'pipe' });
|
|
16
|
+
return result.trim() || null;
|
|
17
|
+
}
|
|
18
|
+
catch {
|
|
19
|
+
return null;
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
export function opStoreKey(project, privateKey, publicKey, vault) {
|
|
23
|
+
const args = [
|
|
24
|
+
'item', 'create',
|
|
25
|
+
'--category', 'password',
|
|
26
|
+
'--title', `${OP_ITEM_PREFIX}${project}`,
|
|
27
|
+
...(vault ? ['--vault', vault] : []),
|
|
28
|
+
`password=${privateKey}`,
|
|
29
|
+
`public_key[text]=${publicKey}`,
|
|
30
|
+
];
|
|
31
|
+
const result = spawnSync('op', args, { stdio: 'pipe', encoding: 'utf-8' });
|
|
32
|
+
if (result.status !== 0) {
|
|
33
|
+
throw new Error(result.stderr || 'Failed to store in 1Password');
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
export function opListKeys(vault) {
|
|
37
|
+
try {
|
|
38
|
+
const vaultArgs = vault ? ['--vault', vault] : [];
|
|
39
|
+
const result = execSync(['op', 'item', 'list', '--categories', 'password', ...vaultArgs, '--format', 'json'].join(' '), { encoding: 'utf-8', stdio: 'pipe' });
|
|
40
|
+
const items = JSON.parse(result);
|
|
41
|
+
return items
|
|
42
|
+
.filter(i => i.title.startsWith(OP_ITEM_PREFIX))
|
|
43
|
+
.map(i => i.title.replace(OP_ITEM_PREFIX, ''));
|
|
44
|
+
}
|
|
45
|
+
catch {
|
|
46
|
+
return [];
|
|
47
|
+
}
|
|
48
|
+
}
|
package/dist/types.d.ts
CHANGED
|
@@ -14,9 +14,12 @@ export interface SourceFiles {
|
|
|
14
14
|
local: string;
|
|
15
15
|
}
|
|
16
16
|
export interface HushConfig {
|
|
17
|
+
schema_version?: number;
|
|
18
|
+
project?: string;
|
|
17
19
|
sources: SourceFiles;
|
|
18
20
|
targets: Target[];
|
|
19
21
|
}
|
|
22
|
+
export declare const CURRENT_SCHEMA_VERSION = 2;
|
|
20
23
|
export interface EnvVar {
|
|
21
24
|
key: string;
|
|
22
25
|
value: string;
|
|
@@ -36,6 +39,7 @@ export interface SetOptions {
|
|
|
36
39
|
root: string;
|
|
37
40
|
file?: 'shared' | 'development' | 'production' | 'local';
|
|
38
41
|
key?: string;
|
|
42
|
+
gui?: boolean;
|
|
39
43
|
}
|
|
40
44
|
export interface RunOptions {
|
|
41
45
|
root: string;
|
|
@@ -64,6 +68,7 @@ export interface CheckOptions {
|
|
|
64
68
|
quiet: boolean;
|
|
65
69
|
onlyChanged: boolean;
|
|
66
70
|
requireSource: boolean;
|
|
71
|
+
allowPlaintext?: boolean;
|
|
67
72
|
}
|
|
68
73
|
export type CheckErrorType = 'SOURCE_MISSING' | 'ENCRYPTED_MISSING' | 'DECRYPT_FAILED' | 'SOPS_NOT_INSTALLED';
|
|
69
74
|
export interface CheckFileResult {
|
|
@@ -75,9 +80,14 @@ export interface CheckFileResult {
|
|
|
75
80
|
changed: string[];
|
|
76
81
|
error?: CheckErrorType;
|
|
77
82
|
}
|
|
83
|
+
export interface PlaintextFileResult {
|
|
84
|
+
file: string;
|
|
85
|
+
keyCount: number;
|
|
86
|
+
}
|
|
78
87
|
export interface CheckResult {
|
|
79
|
-
status: 'ok' | 'drift' | 'error';
|
|
88
|
+
status: 'ok' | 'drift' | 'error' | 'plaintext';
|
|
80
89
|
files: CheckFileResult[];
|
|
90
|
+
plaintextFiles?: PlaintextFileResult[];
|
|
81
91
|
}
|
|
82
92
|
export interface SkillOptions {
|
|
83
93
|
root: string;
|
package/dist/types.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../src/types.ts"],"names":[],"mappings":"AAAA,MAAM,MAAM,YAAY,GAAG,QAAQ,GAAG,UAAU,GAAG,MAAM,GAAG,OAAO,GAAG,MAAM,CAAC;AAC7E,MAAM,MAAM,WAAW,GAAG,aAAa,GAAG,YAAY,CAAC;AAEvD,MAAM,WAAW,MAAM;IACrB,IAAI,EAAE,MAAM,CAAC;IACb,IAAI,EAAE,MAAM,CAAC;IACb,MAAM,EAAE,YAAY,CAAC;IACrB,OAAO,CAAC,EAAE,MAAM,EAAE,CAAC;IACnB,OAAO,CAAC,EAAE,MAAM,EAAE,CAAC;CACpB;AAED,MAAM,WAAW,WAAW;IAC1B,MAAM,EAAE,MAAM,CAAC;IACf,WAAW,EAAE,MAAM,CAAC;IACpB,UAAU,EAAE,MAAM,CAAC;IACnB,KAAK,EAAE,MAAM,CAAC;CACf;AAED,MAAM,WAAW,UAAU;IACzB,OAAO,EAAE,WAAW,CAAC;IACrB,OAAO,EAAE,MAAM,EAAE,CAAC;CACnB;AAED,MAAM,WAAW,MAAM;IACrB,GAAG,EAAE,MAAM,CAAC;IACZ,KAAK,EAAE,MAAM,CAAC;CACf;AAED,MAAM,WAAW,cAAc;IAC7B,GAAG,EAAE,WAAW,CAAC;IACjB,IAAI,EAAE,MAAM,CAAC;CACd;AAED,MAAM,WAAW,cAAc;IAC7B,IAAI,EAAE,MAAM,CAAC;CACd;AAED,MAAM,WAAW,WAAW;IAC1B,IAAI,EAAE,MAAM,CAAC;IACb,IAAI,CAAC,EAAE,QAAQ,GAAG,aAAa,GAAG,YAAY,GAAG,OAAO,CAAC;CAC1D;AAED,MAAM,WAAW,UAAU;IACzB,IAAI,EAAE,MAAM,CAAC;IACb,IAAI,CAAC,EAAE,QAAQ,GAAG,aAAa,GAAG,YAAY,GAAG,OAAO,CAAC;IACzD,GAAG,CAAC,EAAE,MAAM,CAAC;
|
|
1
|
+
{"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../src/types.ts"],"names":[],"mappings":"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"}
|
package/dist/types.js
CHANGED
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@chriscode/hush",
|
|
3
|
-
"version": "2.
|
|
3
|
+
"version": "2.4.0",
|
|
4
4
|
"description": "SOPS-based secrets management for monorepos. Encrypt once, decrypt everywhere.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|
|
@@ -12,6 +12,14 @@
|
|
|
12
12
|
"types": "./dist/index.d.ts"
|
|
13
13
|
}
|
|
14
14
|
},
|
|
15
|
+
"scripts": {
|
|
16
|
+
"build": "tsc",
|
|
17
|
+
"dev": "tsc --watch",
|
|
18
|
+
"test": "vitest run",
|
|
19
|
+
"test:watch": "vitest",
|
|
20
|
+
"prepublishOnly": "pnpm build && pnpm test",
|
|
21
|
+
"type-check": "tsc --noEmit"
|
|
22
|
+
},
|
|
15
23
|
"keywords": [
|
|
16
24
|
"secrets",
|
|
17
25
|
"sops",
|
|
@@ -53,12 +61,5 @@
|
|
|
53
61
|
],
|
|
54
62
|
"publishConfig": {
|
|
55
63
|
"access": "public"
|
|
56
|
-
},
|
|
57
|
-
"scripts": {
|
|
58
|
-
"build": "tsc",
|
|
59
|
-
"dev": "tsc --watch",
|
|
60
|
-
"test": "vitest run",
|
|
61
|
-
"test:watch": "vitest",
|
|
62
|
-
"type-check": "tsc --noEmit"
|
|
63
64
|
}
|
|
64
|
-
}
|
|
65
|
+
}
|
package/LICENSE
DELETED
|
@@ -1,21 +0,0 @@
|
|
|
1
|
-
MIT License
|
|
2
|
-
|
|
3
|
-
Copyright (c) 2025 Chris Hasson
|
|
4
|
-
|
|
5
|
-
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
-
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
-
in the Software without restriction, including without limitation the rights
|
|
8
|
-
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
-
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
-
furnished to do so, subject to the following conditions:
|
|
11
|
-
|
|
12
|
-
The above copyright notice and this permission notice shall be included in all
|
|
13
|
-
copies or substantial portions of the Software.
|
|
14
|
-
|
|
15
|
-
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
-
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
-
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
-
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
-
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
-
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
-
SOFTWARE.
|