@chriscode/hush 4.0.0 → 4.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/dist/cli.js +14 -0
- package/dist/commands/run.d.ts.map +1 -1
- package/dist/commands/run.js +5 -1
- package/dist/commands/set.d.ts.map +1 -1
- package/dist/commands/set.js +32 -1
- package/dist/commands/skill.d.ts.map +1 -1
- package/dist/commands/skill.js +315 -9
- package/dist/commands/status.d.ts.map +1 -1
- package/dist/commands/status.js +13 -42
- package/dist/config/loader.d.ts.map +1 -1
- package/dist/config/loader.js +2 -1
- package/dist/lib/onepassword.d.ts +1 -0
- package/dist/lib/onepassword.d.ts.map +1 -1
- package/dist/lib/onepassword.js +9 -4
- package/package.json +1 -1
package/dist/cli.js
CHANGED
|
@@ -72,12 +72,26 @@ ${pc.bold('Options:')}
|
|
|
72
72
|
-h, --help Show this help message
|
|
73
73
|
-v, --version Show version number
|
|
74
74
|
|
|
75
|
+
${pc.bold('Variable Expansion (v4+):')}
|
|
76
|
+
Subdirectory .env files can reference root secrets:
|
|
77
|
+
|
|
78
|
+
\${VAR} Pull VAR from root secrets
|
|
79
|
+
\${VAR:-default} Pull VAR, use default if missing
|
|
80
|
+
\${env:VAR} Read from system environment (CI, etc.)
|
|
81
|
+
|
|
82
|
+
Example subdirectory template (apps/mobile/.env):
|
|
83
|
+
EXPO_PUBLIC_API_URL=\${API_URL}
|
|
84
|
+
PORT=\${PORT:-3000}
|
|
85
|
+
|
|
86
|
+
Subdirectory templates are safe to commit - they contain no secrets.
|
|
87
|
+
|
|
75
88
|
${pc.bold('Examples:')}
|
|
76
89
|
hush init Initialize config + generate keys
|
|
77
90
|
hush encrypt Encrypt .env files
|
|
78
91
|
hush run -- npm start Run with secrets in memory (AI-safe!)
|
|
79
92
|
hush run -e prod -- npm build Run with production secrets
|
|
80
93
|
hush run -t api -- wrangler dev Run filtered for 'api' target
|
|
94
|
+
cd apps/mobile && hush run -- expo start Run from subdirectory with templates
|
|
81
95
|
hush set DATABASE_URL Set a secret interactively (AI-safe)
|
|
82
96
|
hush set API_KEY --gui Set secret via macOS dialog (for AI agents)
|
|
83
97
|
hush set API_KEY -e prod Set a production secret
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"run.d.ts","sourceRoot":"","sources":["../../src/commands/run.ts"],"names":[],"mappings":"AAWA,OAAO,KAAK,EAAE,UAAU,EAAmC,MAAM,aAAa,CAAC;
|
|
1
|
+
{"version":3,"file":"run.d.ts","sourceRoot":"","sources":["../../src/commands/run.ts"],"names":[],"mappings":"AAWA,OAAO,KAAK,EAAE,UAAU,EAAmC,MAAM,aAAa,CAAC;AAkD/E,wBAAsB,UAAU,CAAC,OAAO,EAAE,UAAU,GAAG,OAAO,CAAC,IAAI,CAAC,CAuFnE"}
|
package/dist/commands/run.js
CHANGED
|
@@ -30,7 +30,11 @@ function getDecryptedSecrets(projectRoot, env, config) {
|
|
|
30
30
|
varSources.push(parseEnvContent(content));
|
|
31
31
|
}
|
|
32
32
|
if (varSources.length === 0) {
|
|
33
|
-
throw new Error(`No encrypted files found
|
|
33
|
+
throw new Error(`No encrypted files found at project root.\n` +
|
|
34
|
+
` Expected: ${sharedEncrypted}\n` +
|
|
35
|
+
` Project root: ${projectRoot}\n\n` +
|
|
36
|
+
` If you haven't encrypted yet, run: npx hush encrypt\n` +
|
|
37
|
+
` If running from a subdirectory, ensure hush.yaml exists at the project root.`);
|
|
34
38
|
}
|
|
35
39
|
const merged = mergeVars(...varSources);
|
|
36
40
|
return interpolateVars(merged);
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"set.d.ts","sourceRoot":"","sources":["../../src/commands/set.ts"],"names":[],"mappings":"AAOA,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,aAAa,CAAC;
|
|
1
|
+
{"version":3,"file":"set.d.ts","sourceRoot":"","sources":["../../src/commands/set.ts"],"names":[],"mappings":"AAOA,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,aAAa,CAAC;AA2M9C,wBAAsB,UAAU,CAAC,OAAO,EAAE,UAAU,GAAG,OAAO,CAAC,IAAI,CAAC,CA0CnE"}
|
package/dist/commands/set.js
CHANGED
|
@@ -1,10 +1,38 @@
|
|
|
1
1
|
import { execSync } from 'node:child_process';
|
|
2
|
-
import { existsSync } from 'node:fs';
|
|
2
|
+
import { existsSync, fstatSync } from 'node:fs';
|
|
3
3
|
import { join } from 'node:path';
|
|
4
4
|
import { platform } from 'node:os';
|
|
5
5
|
import pc from 'picocolors';
|
|
6
6
|
import { loadConfig } from '../config/loader.js';
|
|
7
7
|
import { setKey } from '../core/sops.js';
|
|
8
|
+
const STDIN_FD = 0;
|
|
9
|
+
function hasStdinPipe() {
|
|
10
|
+
try {
|
|
11
|
+
if (process.stdin.isTTY) {
|
|
12
|
+
return false;
|
|
13
|
+
}
|
|
14
|
+
const stat = fstatSync(STDIN_FD);
|
|
15
|
+
return stat.isFIFO() || stat.isFile();
|
|
16
|
+
}
|
|
17
|
+
catch {
|
|
18
|
+
return false;
|
|
19
|
+
}
|
|
20
|
+
}
|
|
21
|
+
function readFromStdinPipe() {
|
|
22
|
+
return new Promise((resolve, reject) => {
|
|
23
|
+
let data = '';
|
|
24
|
+
process.stdin.setEncoding('utf8');
|
|
25
|
+
process.stdin.on('data', (chunk) => {
|
|
26
|
+
data += chunk;
|
|
27
|
+
});
|
|
28
|
+
process.stdin.on('end', () => {
|
|
29
|
+
const trimTrailingNewlines = /\n+$/;
|
|
30
|
+
resolve(data.replace(trimTrailingNewlines, ''));
|
|
31
|
+
});
|
|
32
|
+
process.stdin.on('error', reject);
|
|
33
|
+
process.stdin.resume();
|
|
34
|
+
});
|
|
35
|
+
}
|
|
8
36
|
function promptViaMacOSDialog(key) {
|
|
9
37
|
try {
|
|
10
38
|
const script = `display dialog "Enter value for ${key}:" default answer "" with hidden answer with title "Hush - Set Secret"`;
|
|
@@ -133,6 +161,9 @@ function promptViaTTY(key) {
|
|
|
133
161
|
});
|
|
134
162
|
}
|
|
135
163
|
async function promptForValue(key, forceGui) {
|
|
164
|
+
if (hasStdinPipe()) {
|
|
165
|
+
return readFromStdinPipe();
|
|
166
|
+
}
|
|
136
167
|
if (process.stdin.isTTY && !forceGui) {
|
|
137
168
|
return promptViaTTY(key);
|
|
138
169
|
}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"skill.d.ts","sourceRoot":"","sources":["../../src/commands/skill.ts"],"names":[],"mappings":"AAKA,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,aAAa,CAAC;
|
|
1
|
+
{"version":3,"file":"skill.d.ts","sourceRoot":"","sources":["../../src/commands/skill.ts"],"names":[],"mappings":"AAKA,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,aAAa,CAAC;AAu+ChD,wBAAsB,YAAY,CAAC,OAAO,EAAE,YAAY,GAAG,OAAO,CAAC,IAAI,CAAC,CA0CvE"}
|
package/dist/commands/skill.js
CHANGED
|
@@ -12,9 +12,9 @@ allowed-tools: Bash(hush:*), Bash(npx hush:*), Bash(brew:*), Bash(npm:*), Bash(p
|
|
|
12
12
|
|
|
13
13
|
# Hush - AI-Native Secrets Management
|
|
14
14
|
|
|
15
|
-
**CRITICAL: NEVER read .env files directly.** Always use \`npx hush status\`, \`npx hush inspect\`, or \`npx hush has\` to check secrets.
|
|
15
|
+
**CRITICAL: NEVER read root .env files directly.** Always use \`npx hush status\`, \`npx hush inspect\`, or \`npx hush has\` to check secrets.
|
|
16
16
|
|
|
17
|
-
Hush keeps secrets **encrypted at rest
|
|
17
|
+
Hush keeps secrets **encrypted at rest** at the project root. Subdirectory \`.env\` files are **templates** (safe to commit and read) that reference root secrets via \`\${VAR}\` syntax.
|
|
18
18
|
|
|
19
19
|
## First Step: Investigate Current State
|
|
20
20
|
|
|
@@ -35,12 +35,13 @@ This tells you:
|
|
|
35
35
|
|
|
36
36
|
| You See | What It Means | Action |
|
|
37
37
|
|---------|---------------|--------|
|
|
38
|
-
| \`SECURITY WARNING: Unencrypted .env files\` | Plaintext secrets
|
|
38
|
+
| \`SECURITY WARNING: Unencrypted .env files\` | Plaintext secrets at project root! | Run \`npx hush encrypt\` immediately |
|
|
39
39
|
| \`No hush.yaml found\` | Hush not initialized | Run \`npx hush init\` |
|
|
40
40
|
| \`SOPS not installed\` | Missing prerequisite | \`brew install sops\` |
|
|
41
41
|
| \`age key not found\` | Missing encryption key | \`npx hush keys setup\` |
|
|
42
42
|
| \`Project: not set\` | Key management limited | Add \`project:\` to hush.yaml |
|
|
43
|
-
|
|
43
|
+
|
|
44
|
+
**Note:** Security warnings only apply to root-level \`.env\` files. Subdirectory \`.env\` files are templates (safe to commit).
|
|
44
45
|
|
|
45
46
|
## Decision Tree: What Do I Do?
|
|
46
47
|
|
|
@@ -85,6 +86,122 @@ npx hush run -e production -- npm build # Production
|
|
|
85
86
|
|
|
86
87
|
---
|
|
87
88
|
|
|
89
|
+
## Monorepo Architecture: Push vs Pull
|
|
90
|
+
|
|
91
|
+
Hush supports two ways to distribute secrets in monorepos. **Choose based on the use case:**
|
|
92
|
+
|
|
93
|
+
| Need | Use | Example |
|
|
94
|
+
|------|-----|---------|
|
|
95
|
+
| Pattern-based filtering | **Push** | "All \`NEXT_PUBLIC_*\` vars → web app" |
|
|
96
|
+
| Auto-flow new vars | **Push** | Add var at root, it flows automatically |
|
|
97
|
+
| Rename variables | **Pull** | \`API_URL\` → \`EXPO_PUBLIC_API_URL\` |
|
|
98
|
+
| Default values | **Pull** | \`PORT=\${PORT:-3000}\` |
|
|
99
|
+
| Combine variables | **Pull** | \`URL=\${HOST}:\${PORT}\` |
|
|
100
|
+
|
|
101
|
+
### Push (include/exclude in hush.yaml)
|
|
102
|
+
|
|
103
|
+
Best for simple filtering where new vars should auto-flow:
|
|
104
|
+
|
|
105
|
+
\`\`\`yaml
|
|
106
|
+
# hush.yaml
|
|
107
|
+
targets:
|
|
108
|
+
- name: web
|
|
109
|
+
path: ./apps/web
|
|
110
|
+
include: [NEXT_PUBLIC_*] # All matching vars auto-flow
|
|
111
|
+
\`\`\`
|
|
112
|
+
|
|
113
|
+
### Pull (subdirectory .env templates)
|
|
114
|
+
|
|
115
|
+
Best for transformation, renaming, or explicit dependencies:
|
|
116
|
+
|
|
117
|
+
\`\`\`bash
|
|
118
|
+
# apps/mobile/.env (committed - it's just a template)
|
|
119
|
+
EXPO_PUBLIC_API_URL=\${API_URL} # Rename from root
|
|
120
|
+
PORT=\${PORT:-8081} # Default value
|
|
121
|
+
\`\`\`
|
|
122
|
+
|
|
123
|
+
**Decision rule:** Use push for "all X goes to Y" patterns. Use pull when you need to rename, transform, or add defaults.
|
|
124
|
+
|
|
125
|
+
---
|
|
126
|
+
|
|
127
|
+
## Subdirectory Templates (Pull-Based)
|
|
128
|
+
|
|
129
|
+
When a subdirectory needs to rename, transform, or add defaults to root secrets, create a \`.env\` template file in that subdirectory.
|
|
130
|
+
|
|
131
|
+
### Step-by-Step Setup
|
|
132
|
+
|
|
133
|
+
**1. Ensure root secrets exist:**
|
|
134
|
+
\`\`\`bash
|
|
135
|
+
npx hush inspect # From repo root - verify secrets are configured
|
|
136
|
+
\`\`\`
|
|
137
|
+
|
|
138
|
+
**2. Create subdirectory template (this file is committed to git):**
|
|
139
|
+
\`\`\`bash
|
|
140
|
+
# apps/mobile/.env
|
|
141
|
+
EXPO_PUBLIC_API_URL=\${API_URL} # Pull API_URL from root, rename it
|
|
142
|
+
EXPO_PUBLIC_STRIPE_KEY=\${STRIPE_KEY} # Pull and rename
|
|
143
|
+
PORT=\${PORT:-8081} # Use root PORT, or default to 8081
|
|
144
|
+
DEBUG=\${DEBUG:-false} # Use root DEBUG, or default to false
|
|
145
|
+
\`\`\`
|
|
146
|
+
|
|
147
|
+
**3. Run from the subdirectory:**
|
|
148
|
+
\`\`\`bash
|
|
149
|
+
cd apps/mobile
|
|
150
|
+
npx hush run -- npm start
|
|
151
|
+
\`\`\`
|
|
152
|
+
|
|
153
|
+
Hush automatically:
|
|
154
|
+
1. Finds the project root (where \`hush.yaml\` is)
|
|
155
|
+
2. Decrypts root secrets
|
|
156
|
+
3. Loads the local \`.env\` template
|
|
157
|
+
4. Resolves \`\${VAR}\` references against root secrets
|
|
158
|
+
5. Injects the result into your command
|
|
159
|
+
|
|
160
|
+
### Variable Expansion Syntax
|
|
161
|
+
|
|
162
|
+
| Syntax | Meaning | Example |
|
|
163
|
+
|--------|---------|---------|
|
|
164
|
+
| \`\${VAR}\` | Pull VAR from root secrets | \`API_URL=\${API_URL}\` |
|
|
165
|
+
| \`\${VAR:-default}\` | Pull VAR, use default if missing/empty | \`PORT=\${PORT:-3000}\` |
|
|
166
|
+
| \`\${env:VAR}\` | Read from system environment (CI, etc.) | \`CI=\${env:CI}\` |
|
|
167
|
+
|
|
168
|
+
### Common Patterns
|
|
169
|
+
|
|
170
|
+
**Expo/React Native app:**
|
|
171
|
+
\`\`\`bash
|
|
172
|
+
# apps/mobile/.env
|
|
173
|
+
EXPO_PUBLIC_API_URL=\${API_URL}
|
|
174
|
+
EXPO_PUBLIC_STRIPE_KEY=\${STRIPE_PUBLISHABLE_KEY}
|
|
175
|
+
EXPO_PUBLIC_ENV=\${ENV:-development}
|
|
176
|
+
\`\`\`
|
|
177
|
+
|
|
178
|
+
**Next.js app:**
|
|
179
|
+
\`\`\`bash
|
|
180
|
+
# apps/web/.env
|
|
181
|
+
NEXT_PUBLIC_API_URL=\${API_URL}
|
|
182
|
+
NEXT_PUBLIC_STRIPE_KEY=\${STRIPE_PUBLISHABLE_KEY}
|
|
183
|
+
DATABASE_URL=\${DATABASE_URL}
|
|
184
|
+
\`\`\`
|
|
185
|
+
|
|
186
|
+
**API server with defaults:**
|
|
187
|
+
\`\`\`bash
|
|
188
|
+
# apps/api/.env
|
|
189
|
+
DATABASE_URL=\${DATABASE_URL}
|
|
190
|
+
PORT=\${PORT:-8787}
|
|
191
|
+
LOG_LEVEL=\${LOG_LEVEL:-info}
|
|
192
|
+
\`\`\`
|
|
193
|
+
|
|
194
|
+
### Important Notes
|
|
195
|
+
|
|
196
|
+
- **Subdirectory .env files ARE committed to git** - they're templates, not secrets
|
|
197
|
+
- **Can contain expansions AND constants** - \`APP_NAME=MyApp\` alongside \`API_URL=\${API_URL}\`
|
|
198
|
+
- **Run from the subdirectory** - \`hush run\` auto-detects the project root
|
|
199
|
+
- **Root secrets stay encrypted** - subdirectory templates just reference them
|
|
200
|
+
- **Self-reference works** - \`PORT=\${PORT:-3000}\` uses root PORT if set, else 3000
|
|
201
|
+
- **Security warnings only apply to root** - subdirectory .env files are always safe
|
|
202
|
+
|
|
203
|
+
---
|
|
204
|
+
|
|
88
205
|
## Commands Quick Reference
|
|
89
206
|
|
|
90
207
|
| Command | Purpose | When to Use |
|
|
@@ -196,6 +313,15 @@ npx hush set DEBUG --local # Set personal local override
|
|
|
196
313
|
The user will be prompted to enter the value (hidden input).
|
|
197
314
|
**You never see the actual secret - just invoke the command!**
|
|
198
315
|
|
|
316
|
+
### Add a secret via pipe (for scripts/automation)
|
|
317
|
+
|
|
318
|
+
\`\`\`bash
|
|
319
|
+
echo "my-secret-value" | npx hush set MY_KEY
|
|
320
|
+
cat secret.txt | npx hush set CERT_CONTENT
|
|
321
|
+
\`\`\`
|
|
322
|
+
|
|
323
|
+
When stdin has piped data, Hush reads from it instead of prompting.
|
|
324
|
+
|
|
199
325
|
---
|
|
200
326
|
|
|
201
327
|
## Additional Resources
|
|
@@ -516,6 +642,12 @@ hush set DEBUG --local # Set personal local override
|
|
|
516
642
|
|
|
517
643
|
User will be prompted with hidden input - the value is never visible.
|
|
518
644
|
|
|
645
|
+
**Pipe support:** You can also pipe values directly:
|
|
646
|
+
\`\`\`bash
|
|
647
|
+
echo "my-secret" | hush set MY_KEY
|
|
648
|
+
cat cert.pem | hush set CERTIFICATE
|
|
649
|
+
\`\`\`
|
|
650
|
+
|
|
519
651
|
---
|
|
520
652
|
|
|
521
653
|
### hush edit [file]
|
|
@@ -635,6 +767,43 @@ hush trace STRIPE_SECRET_KEY # Trace another variable
|
|
|
635
767
|
|
|
636
768
|
---
|
|
637
769
|
|
|
770
|
+
### hush template
|
|
771
|
+
|
|
772
|
+
Show the resolved template for the current directory's \`.env\` file.
|
|
773
|
+
|
|
774
|
+
\`\`\`bash
|
|
775
|
+
cd apps/mobile
|
|
776
|
+
hush template # Show resolved expansions
|
|
777
|
+
hush template -e production # Show for production
|
|
778
|
+
\`\`\`
|
|
779
|
+
|
|
780
|
+
**Output shows:**
|
|
781
|
+
- Original template values (e.g., \`\${API_URL}\`)
|
|
782
|
+
- Resolved values from root secrets (masked)
|
|
783
|
+
- Any unresolved references
|
|
784
|
+
|
|
785
|
+
**Use when:** Debugging why a subdirectory template isn't resolving correctly
|
|
786
|
+
|
|
787
|
+
---
|
|
788
|
+
|
|
789
|
+
### hush expansions
|
|
790
|
+
|
|
791
|
+
Show the expansion graph across all subdirectories that have \`.env\` templates.
|
|
792
|
+
|
|
793
|
+
\`\`\`bash
|
|
794
|
+
hush expansions # Scan all subdirectories
|
|
795
|
+
hush expansions -e production # Show for production
|
|
796
|
+
\`\`\`
|
|
797
|
+
|
|
798
|
+
**Output shows:**
|
|
799
|
+
- Which subdirectories have \`.env\` templates
|
|
800
|
+
- What variables each template references from root
|
|
801
|
+
- Resolution status for each reference
|
|
802
|
+
|
|
803
|
+
**Use when:** Getting an overview of pull-based templates across a monorepo
|
|
804
|
+
|
|
805
|
+
---
|
|
806
|
+
|
|
638
807
|
## Quick Reference
|
|
639
808
|
|
|
640
809
|
| Command | Purpose |
|
|
@@ -645,7 +814,10 @@ hush trace STRIPE_SECRET_KEY # Trace another variable
|
|
|
645
814
|
| \`hush inspect\` | See variables (masked) |
|
|
646
815
|
| \`hush has <KEY>\` | Check if variable exists |
|
|
647
816
|
| \`hush status\` | View configuration |
|
|
648
|
-
| \`
|
|
817
|
+
| \`hush resolve <target>\` | See what a target receives |
|
|
818
|
+
| \`hush trace <KEY>\` | Trace variable through targets |
|
|
819
|
+
| \`hush template\` | Show resolved subdirectory template |
|
|
820
|
+
| \`hush expansions\` | Show all subdirectory templates |
|
|
649
821
|
|
|
650
822
|
---
|
|
651
823
|
|
|
@@ -873,14 +1045,32 @@ DEBUG=\${DEBUG:-false}
|
|
|
873
1045
|
PORT=\${PORT:-3000}
|
|
874
1046
|
|
|
875
1047
|
# System environment (explicit opt-in)
|
|
876
|
-
|
|
1048
|
+
CI=\${env:CI}
|
|
877
1049
|
|
|
878
1050
|
# Pull from root (subdirectory .env can reference root secrets)
|
|
879
|
-
# apps/
|
|
880
|
-
|
|
1051
|
+
# apps/mobile/.env:
|
|
1052
|
+
EXPO_PUBLIC_API_URL=\${API_URL} # Renamed from root
|
|
881
1053
|
\`\`\`
|
|
882
1054
|
|
|
883
|
-
**Resolution order:** Local value →
|
|
1055
|
+
**Resolution order:** Local value → Root secrets → System env (only with \`env:\` prefix)
|
|
1056
|
+
|
|
1057
|
+
### Push vs Pull Architecture
|
|
1058
|
+
|
|
1059
|
+
**Push (hush.yaml targets):** Pattern-based filtering, auto-flow
|
|
1060
|
+
\`\`\`yaml
|
|
1061
|
+
targets:
|
|
1062
|
+
- name: web
|
|
1063
|
+
include: [NEXT_PUBLIC_*] # All matching vars flow automatically
|
|
1064
|
+
\`\`\`
|
|
1065
|
+
|
|
1066
|
+
**Pull (subdirectory templates):** Transformation, renaming, defaults
|
|
1067
|
+
\`\`\`bash
|
|
1068
|
+
# apps/mobile/.env
|
|
1069
|
+
EXPO_PUBLIC_API_URL=\${API_URL} # Rename required
|
|
1070
|
+
PORT=\${PORT:-3000} # Default value
|
|
1071
|
+
\`\`\`
|
|
1072
|
+
|
|
1073
|
+
**Decision:** Use push for "all X → Y". Use pull for rename/transform/defaults.
|
|
884
1074
|
`,
|
|
885
1075
|
'examples/workflows.md': `# Hush Workflow Examples
|
|
886
1076
|
|
|
@@ -1110,6 +1300,122 @@ npx hush push
|
|
|
1110
1300
|
|
|
1111
1301
|
---
|
|
1112
1302
|
|
|
1303
|
+
## Setting Up Subdirectory Templates (Pull-Based Secrets)
|
|
1304
|
+
|
|
1305
|
+
### "Set up secrets for a subdirectory app (Expo, Next.js, etc.)"
|
|
1306
|
+
|
|
1307
|
+
**Use this when:** You need to rename, transform, or add defaults to root secrets for a specific package.
|
|
1308
|
+
|
|
1309
|
+
**Step 1: Verify root secrets exist**
|
|
1310
|
+
\`\`\`bash
|
|
1311
|
+
cd /path/to/repo/root
|
|
1312
|
+
npx hush inspect
|
|
1313
|
+
\`\`\`
|
|
1314
|
+
|
|
1315
|
+
**Step 2: Create the subdirectory template file**
|
|
1316
|
+
|
|
1317
|
+
Create a \`.env\` file in the subdirectory. This file is committed to git - it's just a template, not actual secrets.
|
|
1318
|
+
|
|
1319
|
+
\`\`\`bash
|
|
1320
|
+
# Example: apps/mobile/.env
|
|
1321
|
+
EXPO_PUBLIC_API_URL=\${API_URL} # Pulls API_URL from root, renames it
|
|
1322
|
+
EXPO_PUBLIC_STRIPE_KEY=\${STRIPE_KEY} # Pulls and renames
|
|
1323
|
+
PORT=\${PORT:-8081} # Uses root PORT if set, otherwise 8081
|
|
1324
|
+
DEBUG=\${DEBUG:-false} # Uses root DEBUG if set, otherwise false
|
|
1325
|
+
\`\`\`
|
|
1326
|
+
|
|
1327
|
+
**Step 3: Run from the subdirectory**
|
|
1328
|
+
\`\`\`bash
|
|
1329
|
+
cd apps/mobile
|
|
1330
|
+
npx hush run -- npm start
|
|
1331
|
+
\`\`\`
|
|
1332
|
+
|
|
1333
|
+
### Variable Expansion Syntax Reference
|
|
1334
|
+
|
|
1335
|
+
| Syntax | What It Does | Example |
|
|
1336
|
+
|--------|--------------|---------|
|
|
1337
|
+
| \`\${VAR}\` | Pull VAR from root secrets | \`API_URL=\${API_URL}\` |
|
|
1338
|
+
| \`\${VAR:-default}\` | Pull VAR, use default if not set | \`PORT=\${PORT:-3000}\` |
|
|
1339
|
+
| \`\${env:VAR}\` | Read from system environment | \`CI=\${env:CI}\` |
|
|
1340
|
+
|
|
1341
|
+
### Framework Examples
|
|
1342
|
+
|
|
1343
|
+
**Expo/React Native:**
|
|
1344
|
+
\`\`\`bash
|
|
1345
|
+
# apps/mobile/.env
|
|
1346
|
+
EXPO_PUBLIC_API_URL=\${API_URL}
|
|
1347
|
+
EXPO_PUBLIC_STRIPE_KEY=\${STRIPE_PUBLISHABLE_KEY}
|
|
1348
|
+
EXPO_PUBLIC_ENV=\${ENV:-development}
|
|
1349
|
+
\`\`\`
|
|
1350
|
+
|
|
1351
|
+
**Next.js:**
|
|
1352
|
+
\`\`\`bash
|
|
1353
|
+
# apps/web/.env
|
|
1354
|
+
NEXT_PUBLIC_API_URL=\${API_URL}
|
|
1355
|
+
NEXT_PUBLIC_STRIPE_KEY=\${STRIPE_PUBLISHABLE_KEY}
|
|
1356
|
+
DATABASE_URL=\${DATABASE_URL}
|
|
1357
|
+
\`\`\`
|
|
1358
|
+
|
|
1359
|
+
**Cloudflare Worker:**
|
|
1360
|
+
\`\`\`bash
|
|
1361
|
+
# apps/api/.env
|
|
1362
|
+
DATABASE_URL=\${DATABASE_URL}
|
|
1363
|
+
STRIPE_SECRET_KEY=\${STRIPE_SECRET_KEY}
|
|
1364
|
+
PORT=\${PORT:-8787}
|
|
1365
|
+
\`\`\`
|
|
1366
|
+
|
|
1367
|
+
### Important Notes
|
|
1368
|
+
|
|
1369
|
+
- **Template files ARE committed** to git (they contain no secrets)
|
|
1370
|
+
- **Root secrets stay encrypted** - templates just reference them
|
|
1371
|
+
- **Run from subdirectory** - \`hush run\` finds the project root automatically
|
|
1372
|
+
- **Self-reference works** - \`PORT=\${PORT:-3000}\` uses root PORT if set
|
|
1373
|
+
|
|
1374
|
+
---
|
|
1375
|
+
|
|
1376
|
+
## Choosing Push vs Pull (Monorepos)
|
|
1377
|
+
|
|
1378
|
+
### "How should I set up secrets for a new package?"
|
|
1379
|
+
|
|
1380
|
+
**Ask yourself:** Does this package need to rename variables or add defaults?
|
|
1381
|
+
|
|
1382
|
+
#### If NO (simple filtering) → Use Push
|
|
1383
|
+
|
|
1384
|
+
Edit \`hush.yaml\` to add a target:
|
|
1385
|
+
\`\`\`yaml
|
|
1386
|
+
targets:
|
|
1387
|
+
- name: new-package
|
|
1388
|
+
path: ./packages/new-package
|
|
1389
|
+
format: dotenv
|
|
1390
|
+
include:
|
|
1391
|
+
- NEXT_PUBLIC_* # Or whatever pattern fits
|
|
1392
|
+
\`\`\`
|
|
1393
|
+
|
|
1394
|
+
**Benefits:** New \`NEXT_PUBLIC_*\` vars at root auto-flow. Zero maintenance.
|
|
1395
|
+
|
|
1396
|
+
#### If YES (transformation needed) → Use Pull
|
|
1397
|
+
|
|
1398
|
+
Create a template \`.env\` in the package:
|
|
1399
|
+
\`\`\`bash
|
|
1400
|
+
# packages/mobile/.env (committed to git)
|
|
1401
|
+
EXPO_PUBLIC_API_URL=\${API_URL} # Rename from root
|
|
1402
|
+
EXPO_PUBLIC_DEBUG=\${DEBUG:-false} # With default
|
|
1403
|
+
PORT=\${PORT:-8081} # Local default
|
|
1404
|
+
\`\`\`
|
|
1405
|
+
|
|
1406
|
+
**Benefits:** Full control over naming and defaults. Explicit dependencies.
|
|
1407
|
+
|
|
1408
|
+
### "When do I update templates vs hush.yaml?"
|
|
1409
|
+
|
|
1410
|
+
| Scenario | Update |
|
|
1411
|
+
|----------|--------|
|
|
1412
|
+
| New \`NEXT_PUBLIC_*\` var, web uses push | Nothing! Auto-flows |
|
|
1413
|
+
| New var mobile needs, mobile uses pull | \`packages/mobile/.env\` template |
|
|
1414
|
+
| New package needs secrets | \`hush.yaml\` (push) or new template (pull) |
|
|
1415
|
+
| Change var routing | \`hush.yaml\` include/exclude patterns |
|
|
1416
|
+
|
|
1417
|
+
---
|
|
1418
|
+
|
|
1113
1419
|
## Understanding the Output
|
|
1114
1420
|
|
|
1115
1421
|
### npx hush status output explained
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"status.d.ts","sourceRoot":"","sources":["../../src/commands/status.ts"],"names":[],"mappings":"AAQA,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,aAAa,CAAC;
|
|
1
|
+
{"version":3,"file":"status.d.ts","sourceRoot":"","sources":["../../src/commands/status.ts"],"names":[],"mappings":"AAQA,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,aAAa,CAAC;AAwCjD,wBAAsB,aAAa,CAAC,OAAO,EAAE,aAAa,GAAG,OAAO,CAAC,IAAI,CAAC,CAmHzE"}
|
package/dist/commands/status.js
CHANGED
|
@@ -1,45 +1,21 @@
|
|
|
1
|
-
import { existsSync
|
|
1
|
+
import { existsSync } from 'node:fs';
|
|
2
2
|
import { join } from 'node:path';
|
|
3
3
|
import pc from 'picocolors';
|
|
4
4
|
import { findConfigPath, loadConfig } from '../config/loader.js';
|
|
5
5
|
import { describeFilter } from '../core/filter.js';
|
|
6
6
|
import { isAgeKeyConfigured, isSopsInstalled } from '../core/sops.js';
|
|
7
7
|
import { keyExists } from '../lib/age.js';
|
|
8
|
-
import {
|
|
8
|
+
import { opInstalled } from '../lib/onepassword.js';
|
|
9
9
|
import { FORMAT_OUTPUT_FILES } from '../types.js';
|
|
10
|
-
function
|
|
10
|
+
function findRootPlaintextEnvFiles(root) {
|
|
11
11
|
const results = [];
|
|
12
12
|
const plaintextPatterns = ['.env', '.env.development', '.env.production', '.env.local', '.env.staging', '.env.test', '.dev.vars'];
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
entries = readdirSync(dir);
|
|
18
|
-
}
|
|
19
|
-
catch {
|
|
20
|
-
return;
|
|
21
|
-
}
|
|
22
|
-
for (const entry of entries) {
|
|
23
|
-
if (skipDirs.has(entry))
|
|
24
|
-
continue;
|
|
25
|
-
if (entry.endsWith('.encrypted'))
|
|
26
|
-
continue;
|
|
27
|
-
const fullPath = join(dir, entry);
|
|
28
|
-
const relPath = relativePath ? `${relativePath}/${entry}` : entry;
|
|
29
|
-
try {
|
|
30
|
-
if (statSync(fullPath).isDirectory()) {
|
|
31
|
-
scanDir(fullPath, relPath);
|
|
32
|
-
}
|
|
33
|
-
else if (plaintextPatterns.includes(entry)) {
|
|
34
|
-
results.push(relPath);
|
|
35
|
-
}
|
|
36
|
-
}
|
|
37
|
-
catch {
|
|
38
|
-
continue;
|
|
39
|
-
}
|
|
13
|
+
for (const pattern of plaintextPatterns) {
|
|
14
|
+
const filePath = join(root, pattern);
|
|
15
|
+
if (existsSync(filePath)) {
|
|
16
|
+
results.push(pattern);
|
|
40
17
|
}
|
|
41
18
|
}
|
|
42
|
-
scanDir(root);
|
|
43
19
|
return results;
|
|
44
20
|
}
|
|
45
21
|
function getProjectFromConfig(root) {
|
|
@@ -72,10 +48,10 @@ export async function statusCommand(options) {
|
|
|
72
48
|
const config = loadConfig(root);
|
|
73
49
|
const configPath = findConfigPath(root);
|
|
74
50
|
console.log(pc.blue('Hush Status\n'));
|
|
75
|
-
const plaintextFiles =
|
|
51
|
+
const plaintextFiles = findRootPlaintextEnvFiles(root);
|
|
76
52
|
if (plaintextFiles.length > 0) {
|
|
77
53
|
console.log(pc.bgRed(pc.white(pc.bold(' SECURITY WARNING '))));
|
|
78
|
-
console.log(pc.red(pc.bold('\nUnencrypted .env files detected!\n')));
|
|
54
|
+
console.log(pc.red(pc.bold('\nUnencrypted .env files detected at project root!\n')));
|
|
79
55
|
for (const file of plaintextFiles) {
|
|
80
56
|
console.log(pc.red(` ${file}`));
|
|
81
57
|
}
|
|
@@ -112,21 +88,16 @@ export async function statusCommand(options) {
|
|
|
112
88
|
: pc.yellow(' age key not found at ~/.config/sops/age/key.txt'));
|
|
113
89
|
if (project) {
|
|
114
90
|
const hasLocalKey = keyExists(project);
|
|
115
|
-
const has1PasswordBackup = opAvailable() && opListKeys().includes(project);
|
|
116
91
|
console.log(pc.bold('\nKey Status:'));
|
|
117
92
|
console.log(hasLocalKey
|
|
118
93
|
? pc.green(` Local key: ~/.config/sops/age/keys/${project.replace(/\//g, '-')}.txt`)
|
|
119
94
|
: pc.yellow(' Local key: not found'));
|
|
120
|
-
if (
|
|
121
|
-
console.log(
|
|
122
|
-
|
|
123
|
-
: pc.yellow(' 1Password backup: not synced'));
|
|
124
|
-
if (!has1PasswordBackup && hasLocalKey) {
|
|
125
|
-
console.log(pc.dim(' Run "npx hush keys push" to backup to 1Password'));
|
|
126
|
-
}
|
|
95
|
+
if (opInstalled()) {
|
|
96
|
+
console.log(pc.dim(' 1Password CLI: installed'));
|
|
97
|
+
console.log(pc.dim(' Run "npx hush keys list" to check backup status'));
|
|
127
98
|
}
|
|
128
99
|
else {
|
|
129
|
-
console.log(pc.dim(' 1Password CLI: not
|
|
100
|
+
console.log(pc.dim(' 1Password CLI: not installed'));
|
|
130
101
|
}
|
|
131
102
|
if (!hasLocalKey) {
|
|
132
103
|
console.log(pc.bold('\n To set up keys:'));
|
|
@@ -1 +1 @@
|
|
|
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,eAAe,CAAC,QAAQ,EAAE,MAAM,GAAG;IAAE,UAAU,EAAE,MAAM,CAAC;IAAC,WAAW,EAAE,MAAM,CAAA;CAAE,GAAG,IAAI,CAepG;AAED,wBAAgB,UAAU,CAAC,IAAI,EAAE,MAAM,GAAG,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,eAAe,CAAC,QAAQ,EAAE,MAAM,GAAG;IAAE,UAAU,EAAE,MAAM,CAAC;IAAC,WAAW,EAAE,MAAM,CAAA;CAAE,GAAG,IAAI,CAepG;AAED,wBAAgB,UAAU,CAAC,IAAI,EAAE,MAAM,GAAG,UAAU,CAoBnD;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,CA8B3D"}
|
package/dist/config/loader.js
CHANGED
|
@@ -37,7 +37,8 @@ export function loadConfig(root) {
|
|
|
37
37
|
const content = readFileSync(configPath, 'utf-8');
|
|
38
38
|
const parsed = parseYaml(content);
|
|
39
39
|
return {
|
|
40
|
-
version
|
|
40
|
+
// Support both 'version' and 'schema_version' (prefer schema_version)
|
|
41
|
+
version: parsed.schema_version ?? parsed.version,
|
|
41
42
|
project: parsed.project,
|
|
42
43
|
sources: { ...DEFAULT_SOURCES, ...parsed.sources },
|
|
43
44
|
targets: parsed.targets ?? [{ name: 'root', path: '.', format: 'dotenv' }],
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
export declare const OP_ITEM_PREFIX = "SOPS Key - hush/";
|
|
2
|
+
export declare function opInstalled(): boolean;
|
|
2
3
|
export declare function opAvailable(): boolean;
|
|
3
4
|
export declare function opGetKey(project: string, vault?: string): string | null;
|
|
4
5
|
export declare function opStoreKey(project: string, privateKey: string, publicKey: string, vault?: string): void;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"onepassword.d.ts","sourceRoot":"","sources":["../../src/lib/onepassword.ts"],"names":[],"mappings":"AAEA,eAAO,MAAM,cAAc,qBAAqB,CAAC;
|
|
1
|
+
{"version":3,"file":"onepassword.d.ts","sourceRoot":"","sources":["../../src/lib/onepassword.ts"],"names":[],"mappings":"AAEA,eAAO,MAAM,cAAc,qBAAqB,CAAC;AAUjD,wBAAgB,WAAW,IAAI,OAAO,CAOrC;AAED,wBAAgB,WAAW,IAAI,OAAO,CAOrC;AAED,wBAAgB,QAAQ,CAAC,OAAO,EAAE,MAAM,EAAE,KAAK,CAAC,EAAE,MAAM,GAAG,MAAM,GAAG,IAAI,CASvE;AAED,wBAAgB,UAAU,CAAC,OAAO,EAAE,MAAM,EAAE,UAAU,EAAE,MAAM,EAAE,SAAS,EAAE,MAAM,EAAE,KAAK,CAAC,EAAE,MAAM,GAAG,IAAI,CAiBvG;AAED,wBAAgB,UAAU,CAAC,KAAK,CAAC,EAAE,MAAM,GAAG,MAAM,EAAE,CAYnD"}
|
package/dist/lib/onepassword.js
CHANGED
|
@@ -1,9 +1,5 @@
|
|
|
1
1
|
import { execSync } from 'node:child_process';
|
|
2
2
|
export const OP_ITEM_PREFIX = 'SOPS Key - hush/';
|
|
3
|
-
/**
|
|
4
|
-
* 1Password CLI sessions don't persist across subprocesses, so we run
|
|
5
|
-
* `op signin` before every command to trigger biometric auth.
|
|
6
|
-
*/
|
|
7
3
|
function opExec(command) {
|
|
8
4
|
return execSync(`op signin && ${command}`, {
|
|
9
5
|
encoding: 'utf-8',
|
|
@@ -11,6 +7,15 @@ function opExec(command) {
|
|
|
11
7
|
shell: '/bin/bash',
|
|
12
8
|
});
|
|
13
9
|
}
|
|
10
|
+
export function opInstalled() {
|
|
11
|
+
try {
|
|
12
|
+
execSync('which op', { encoding: 'utf-8', stdio: 'pipe' });
|
|
13
|
+
return true;
|
|
14
|
+
}
|
|
15
|
+
catch {
|
|
16
|
+
return false;
|
|
17
|
+
}
|
|
18
|
+
}
|
|
14
19
|
export function opAvailable() {
|
|
15
20
|
try {
|
|
16
21
|
opExec('op whoami');
|