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.
@@ -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
+ };