env-secrets 0.3.3 → 0.5.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/AGENTS.md +12 -3
- package/README.md +52 -8
- package/__e2e__/README.md +2 -5
- package/__e2e__/index.test.ts +341 -2
- package/__e2e__/utils/test-utils.ts +61 -1
- package/__tests__/cli/helpers.test.ts +217 -0
- package/__tests__/vaults/aws-config.test.ts +85 -0
- package/__tests__/vaults/secretsmanager-admin.test.ts +355 -0
- package/dist/cli/helpers.js +172 -0
- package/dist/index.js +456 -2
- package/dist/vaults/aws-config.js +29 -0
- package/dist/vaults/secretsmanager-admin.js +273 -0
- package/dist/vaults/secretsmanager.js +8 -16
- package/docs/AWS.md +129 -2
- package/jest.e2e.config.js +1 -0
- package/package.json +5 -5
- package/src/cli/helpers.ts +239 -0
- package/src/index.ts +595 -2
- package/src/vaults/aws-config.ts +51 -0
- package/src/vaults/secretsmanager-admin.ts +397 -0
- package/src/vaults/secretsmanager.ts +8 -21
- package/website/docs/cli-reference.mdx +101 -0
- package/website/docs/providers/aws-secrets-manager.mdx +59 -0
|
@@ -0,0 +1,239 @@
|
|
|
1
|
+
import { readFile } from 'node:fs/promises';
|
|
2
|
+
|
|
3
|
+
export type OutputFormat = 'json' | 'table';
|
|
4
|
+
export interface AwsScopeOptions {
|
|
5
|
+
profile?: string;
|
|
6
|
+
region?: string;
|
|
7
|
+
}
|
|
8
|
+
export interface EnvSecretEntry {
|
|
9
|
+
key: string;
|
|
10
|
+
value: string;
|
|
11
|
+
line: number;
|
|
12
|
+
}
|
|
13
|
+
export interface ParsedEnvSecrets {
|
|
14
|
+
entries: EnvSecretEntry[];
|
|
15
|
+
skipped: Array<{ key: string; line: number; reason: string }>;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
interface CommandLikeWithGlobalOpts {
|
|
19
|
+
optsWithGlobals?: () => Record<string, unknown>;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
export const asOutputFormat = (value: string): OutputFormat => {
|
|
23
|
+
if (value !== 'json' && value !== 'table') {
|
|
24
|
+
throw new Error(`Invalid output format "${value}". Use "json" or "table".`);
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
return value;
|
|
28
|
+
};
|
|
29
|
+
|
|
30
|
+
export const renderTable = (
|
|
31
|
+
headers: Array<{ key: string; label: string }>,
|
|
32
|
+
rows: Array<Record<string, string | undefined>>
|
|
33
|
+
) => {
|
|
34
|
+
if (rows.length === 0) {
|
|
35
|
+
return 'No results.';
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
const widths = headers.map((header) => {
|
|
39
|
+
return Math.max(
|
|
40
|
+
header.label.length,
|
|
41
|
+
...rows.map((row) => String(row[header.key] || '').length)
|
|
42
|
+
);
|
|
43
|
+
});
|
|
44
|
+
|
|
45
|
+
const headerLine = headers
|
|
46
|
+
.map((header, index) => header.label.padEnd(widths[index]))
|
|
47
|
+
.join(' ');
|
|
48
|
+
const divider = headers
|
|
49
|
+
.map((_, index) => '-'.repeat(widths[index]))
|
|
50
|
+
.join(' ');
|
|
51
|
+
const lines = rows.map((row) =>
|
|
52
|
+
headers
|
|
53
|
+
.map((header, index) =>
|
|
54
|
+
String(row[header.key] || '').padEnd(widths[index])
|
|
55
|
+
)
|
|
56
|
+
.join(' ')
|
|
57
|
+
);
|
|
58
|
+
|
|
59
|
+
return [headerLine, divider, ...lines].join('\n');
|
|
60
|
+
};
|
|
61
|
+
|
|
62
|
+
export const printData = (
|
|
63
|
+
format: OutputFormat,
|
|
64
|
+
headers: Array<{ key: string; label: string }>,
|
|
65
|
+
rows: Array<Record<string, string | undefined>>
|
|
66
|
+
) => {
|
|
67
|
+
if (format === 'json') {
|
|
68
|
+
// eslint-disable-next-line no-console
|
|
69
|
+
console.log(JSON.stringify(rows, null, 2));
|
|
70
|
+
return;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
// eslint-disable-next-line no-console
|
|
74
|
+
console.log(renderTable(headers, rows));
|
|
75
|
+
};
|
|
76
|
+
|
|
77
|
+
export const parseRecoveryDays = (value: string) => {
|
|
78
|
+
const parsed = Number(value);
|
|
79
|
+
if (!Number.isInteger(parsed) || parsed < 7 || parsed > 30) {
|
|
80
|
+
throw new Error('Recovery days must be an integer between 7 and 30.');
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
return parsed;
|
|
84
|
+
};
|
|
85
|
+
|
|
86
|
+
export const readStdin = async (stdin: NodeJS.ReadStream = process.stdin) => {
|
|
87
|
+
const chunks: Buffer[] = [];
|
|
88
|
+
|
|
89
|
+
return await new Promise<string>((resolve, reject) => {
|
|
90
|
+
const onData = (chunk: Buffer) => {
|
|
91
|
+
chunks.push(chunk);
|
|
92
|
+
};
|
|
93
|
+
const onEnd = () => {
|
|
94
|
+
cleanup();
|
|
95
|
+
resolve(
|
|
96
|
+
Buffer.concat(chunks)
|
|
97
|
+
.toString('utf8')
|
|
98
|
+
.replace(/\r?\n$/, '')
|
|
99
|
+
);
|
|
100
|
+
};
|
|
101
|
+
const onError = (error: Error) => {
|
|
102
|
+
cleanup();
|
|
103
|
+
reject(error);
|
|
104
|
+
};
|
|
105
|
+
const cleanup = () => {
|
|
106
|
+
stdin.off('data', onData);
|
|
107
|
+
stdin.off('end', onEnd);
|
|
108
|
+
stdin.off('error', onError);
|
|
109
|
+
};
|
|
110
|
+
|
|
111
|
+
stdin.on('data', onData);
|
|
112
|
+
stdin.once('end', onEnd);
|
|
113
|
+
stdin.once('error', onError);
|
|
114
|
+
});
|
|
115
|
+
};
|
|
116
|
+
|
|
117
|
+
export const resolveSecretValue = async (
|
|
118
|
+
value?: string,
|
|
119
|
+
valueStdin?: boolean,
|
|
120
|
+
valueFile?: string
|
|
121
|
+
): Promise<string | undefined> => {
|
|
122
|
+
const providedSources = [
|
|
123
|
+
value !== undefined,
|
|
124
|
+
valueStdin === true,
|
|
125
|
+
valueFile !== undefined
|
|
126
|
+
].filter(Boolean).length;
|
|
127
|
+
if (providedSources > 1) {
|
|
128
|
+
throw new Error(
|
|
129
|
+
'Use only one secret value source: --value, --value-stdin, or --file.'
|
|
130
|
+
);
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
if (valueStdin) {
|
|
134
|
+
if (process.stdin.isTTY) {
|
|
135
|
+
throw new Error(
|
|
136
|
+
'No stdin detected. Pipe a value when using --value-stdin.'
|
|
137
|
+
);
|
|
138
|
+
}
|
|
139
|
+
return await readStdin();
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
if (valueFile) {
|
|
143
|
+
const content = await readFile(valueFile, 'utf8');
|
|
144
|
+
return content.replace(/\r?\n$/, '');
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
return value;
|
|
148
|
+
};
|
|
149
|
+
|
|
150
|
+
const parseEnvLine = (
|
|
151
|
+
line: string,
|
|
152
|
+
lineNumber: number
|
|
153
|
+
): { key: string; value: string } | undefined => {
|
|
154
|
+
const trimmed = line.trim();
|
|
155
|
+
if (!trimmed || trimmed.startsWith('#')) {
|
|
156
|
+
return undefined;
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
const candidate = trimmed.startsWith('export ')
|
|
160
|
+
? trimmed.slice('export '.length).trimStart()
|
|
161
|
+
: trimmed;
|
|
162
|
+
const separatorIndex = candidate.indexOf('=');
|
|
163
|
+
|
|
164
|
+
if (separatorIndex <= 0) {
|
|
165
|
+
throw new Error(
|
|
166
|
+
`Malformed env line ${lineNumber}. Expected KEY=value or export KEY=value.`
|
|
167
|
+
);
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
const key = candidate.slice(0, separatorIndex).trim();
|
|
171
|
+
const value = candidate.slice(separatorIndex + 1).trim();
|
|
172
|
+
|
|
173
|
+
if (!key) {
|
|
174
|
+
throw new Error(
|
|
175
|
+
`Malformed env line ${lineNumber}. Expected KEY=value or export KEY=value.`
|
|
176
|
+
);
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
return { key, value };
|
|
180
|
+
};
|
|
181
|
+
|
|
182
|
+
export const parseEnvSecrets = (content: string): ParsedEnvSecrets => {
|
|
183
|
+
const seenKeys = new Set<string>();
|
|
184
|
+
const entries: EnvSecretEntry[] = [];
|
|
185
|
+
const skipped: Array<{ key: string; line: number; reason: string }> = [];
|
|
186
|
+
|
|
187
|
+
const lines = content.split(/\r?\n/);
|
|
188
|
+
for (let index = 0; index < lines.length; index += 1) {
|
|
189
|
+
const parsed = parseEnvLine(lines[index], index + 1);
|
|
190
|
+
if (!parsed) {
|
|
191
|
+
continue;
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
if (seenKeys.has(parsed.key)) {
|
|
195
|
+
skipped.push({
|
|
196
|
+
key: parsed.key,
|
|
197
|
+
line: index + 1,
|
|
198
|
+
reason: 'duplicate key'
|
|
199
|
+
});
|
|
200
|
+
continue;
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
seenKeys.add(parsed.key);
|
|
204
|
+
entries.push({
|
|
205
|
+
key: parsed.key,
|
|
206
|
+
value: parsed.value,
|
|
207
|
+
line: index + 1
|
|
208
|
+
});
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
return { entries, skipped };
|
|
212
|
+
};
|
|
213
|
+
|
|
214
|
+
export const parseEnvSecretsFile = async (
|
|
215
|
+
path: string
|
|
216
|
+
): Promise<ParsedEnvSecrets> => {
|
|
217
|
+
const content = await readFile(path, 'utf8');
|
|
218
|
+
return parseEnvSecrets(content);
|
|
219
|
+
};
|
|
220
|
+
|
|
221
|
+
export const resolveAwsScope = (
|
|
222
|
+
options: AwsScopeOptions,
|
|
223
|
+
command?: CommandLikeWithGlobalOpts
|
|
224
|
+
): AwsScopeOptions => {
|
|
225
|
+
const globalOptions = command?.optsWithGlobals?.() || {};
|
|
226
|
+
|
|
227
|
+
const profile =
|
|
228
|
+
options.profile ||
|
|
229
|
+
(typeof globalOptions.profile === 'string'
|
|
230
|
+
? globalOptions.profile
|
|
231
|
+
: undefined);
|
|
232
|
+
const region =
|
|
233
|
+
options.region ||
|
|
234
|
+
(typeof globalOptions.region === 'string'
|
|
235
|
+
? globalOptions.region
|
|
236
|
+
: undefined);
|
|
237
|
+
|
|
238
|
+
return { profile, region };
|
|
239
|
+
};
|